IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> 05-单点登陆系统(SSO)设计及实现 -> 正文阅读

[Java知识库]05-单点登陆系统(SSO)设计及实现

系统简介

Http协议

web应用采用browser/server架构,http作为通信协议。http是无状态协议,浏览器的每一次请求,服务器会独立处理,不与之前或之后的请求产生关联,这个过程用下图说明,三次请求/响应对之间没有任何联系。

在这里插入图片描述

但这也同时意味着,任何用户都能通过浏览器访问服务器资源,如果想保护服务器的某些资源,必须限制浏览器请求;要限制浏览器请求,必须鉴别浏览器请求,响应合法请求,忽略非法请求;要鉴别浏览器请求,必须清楚浏览器请求状态。既然http协议无状态,那就让服务器和浏览器共同维护一个状态吧!这就是会话机制。

有状态会话

浏览器第一次请求服务器,服务器创建一个会话,并将会话的id作为响应的一部分发送给浏览器,浏览器存储会话id,并在后续第二次和第三次请求中带上会话id,服务器取得请求中的会话id就知道是不是同一个用户了,这个过程用下图说明,后续请求与第一次请求产生了关联。
在这里插入图片描述

服务器在内存中保存会话对象,浏览器怎么保存会话id呢?那就浏览器自己来维护这个会话id,每次发送http请求时浏览器自动发送会话id,cookie机制正好用来做这件事。cookie是浏览器用来存储少量数据的一种机制,数据以”key/value“形式存储,浏览器发送http请求时自动附带cookie信息
tomcat会话机制当然也实现了cookie,访问tomcat服务器时,浏览器中可以看到一个名为“JSESSIONID”的cookie,这就是tomcat会话机制维护的会话id,使用了cookie的请求响应过程如下图
在这里插入图片描述

记录登陆状态

有了会话机制,登录状态就好明白了,我们假设浏览器第一次请求服务器需要输入用户名与密码验证身份,服务器拿到用户名密码去数据库比对,正确的话说明当前持有这个会话的用户是合法用户,应该将这个会话状态进行保存,例如:

在这里插入图片描述

单点登录系统设计

概述

web系统早已从久远的单系统发展成为如今由多系统组成的应用群,面对如此众多的系统,用户难道要一个一个登录、然后一个一个注销吗?就像下图描述的这样,例如
在这里插入图片描述

Web系统由单系统发展成多系统组成的应用群,复杂性应该由系统内部承担,而不是用户。无论web系统内部多么复杂,对用户而言,都是一个统一的整体,也就是说,用户访问web系统的整个应用群与访问单个系统一样,登录/注销只要一次就够了
在这里插入图片描述
虽然单系统的登录解决方案很完美,但对于多系统应用群已经不再适用了,为什么呢?
单系统登录解决方案的核心是cookie,cookie携带会话id在浏览器与服务器之间维护会话状态。但cookie是有限制的,这个限制就是cookie的域(通常对应网站的域名),浏览器发送http请求时会自动携带与该域匹配的cookie,而不是所有cookie,例如:
在这里插入图片描述
既然这样,为什么不将web应用群中所有子系统的域名统一在一个顶级域名下,例如“*.baidu.com”,然后将它们的cookie域设置为“baidu.com”,这种做法理论上是可以的,甚至早期很多多系统登录就采用这种同域名共享cookie的方式。
然而,可行并不代表好,共享cookie的方式存在众多局限。首先,应用群域名得统一;其次,应用群各系统使用的技术(至少是web服务器)要相同,不然cookie的key值(tomcat为JSESSIONID)不同,无法维持会话,共享cookie的方式是无法实现跨语言技术平台登录的,比如java、php、.net系统之间;第三,cookie本身不安全。
因此,我们需要一种全新的登录方式来实现多系统应用群的登录,这就是单点登录, 什么是单点登录?单点登录全称Single Sign On(以下简称SSO),是指在多系统应用群中登录一个系统,便可在其他所有系统中得到授权而无需再次登录,包括单点登录与单点注销两部分。

登陆业务设计

相比于单系统登录,sso需要一个独立的认证中心,只有认证中心能接受用户的用户名密码等安全信息,其他系统不提供登录入口,只接受认证中心的间接授权。间接授权通过令牌实现,sso认证中心验证用户的用户名密码没问题,创建授权令牌,在接下来的跳转过程中,授权令牌作为参数发送给各个子系统,子系统拿到令牌,即得到了授权,可以借此创建会话,例如:
在这里插入图片描述

创建项目聚合工程

创建聚合工程的目的是对项目中的资源(例如一些依赖)进行统一管理,多个项目module之间共享资源.
这次项目的maven工程结构如下:
|—04-jt-sso
|–sso-auth #认证服务器
|–sso-resource #资源服务器
|–pom.xml #公共依赖及版本管理

创建父工程

第一步:创建父工程,名字为04-jt-sso,例如:
在这里插入图片描述
第二步:删除工程中的src目录(parent工程一般不需要写java代码).
第三步:在pom.xml文件中添加parent元素并指定springboot依赖.

 <parent>
        <artifactId>spring-boot-starter-parent</artifactId>
        <groupId>org.springframework.boot</groupId>
        <version>2.3.2.RELEASE</version>
</parent>

创建认证工程

第一步:在04-jt-sso工程下创建sso-auth项目module,例如
在这里插入图片描述
第二步:打开pom.xml文件,添加项目依赖

   <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>
    </dependencies>

第三步:创建application.yml配置文件,定义服务端口,例如

server:
    port: 8081

第四步:编写项目启动类.

package sso;
@SpringBootApplication
public class AuthApp {
    public static void main(String[] args) {
        SpringApplication.run(AuthApp.class,args);
    }
}

第五步:启动服务检测是否可以启动ok.

创建资源工程

第一步:在04-jt-sso工程下创建sso-resource项目module,例如
在这里插入图片描述
第二步:打开pom.xml文件,添加项目依赖

 <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>
    </dependencies>

第三步:创建application.yml配置文件,定义服务端口,例如

server:
  port: 8091

第四步:编写项目启动类.

package sso;
@SpringBootApplication
public class ResApp {
    public static void main(String[] args) {
        SpringApplication.run(ResApp.class,args);
    }
}

认证服务实现

工具类

第一步:定义JWT工具类,用于创建,解析,验证token,代码如下:

package sso.auth.util;
public class JwtUtils {
    private static String secret="AAABBBCCCDDDEEE";
    /**基于负载和算法创建token信息*/
    public static String generatorToken(Map<String,Object> map){
        return Jwts.builder()
                .setClaims(map)
                .setExpiration(new Date(System.currentTimeMillis()+30*60*1000))
                .setIssuedAt(new Date())
                .signWith(SignatureAlgorithm.HS256,secret)
                .compact();//签约,创建token
    }
    /**解析token获取数据*/
    public static Claims getClaimsFromToken(String token){
        return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
    }
    /**判定token是否失效*/
    public static boolean isTokenExpired(String token){
        Date expiration=getClaimsFromToken(token).getExpiration();
        return expiration.before(new Date());
    }
}

第二步:定义Web工具类,用于向客户端响应json数据

package sso.auth.util;
public class WebUtils {
    public static void writeJsonToClient(HttpServletResponse response, Map<String,Object> map)
            throws IOException {
        //1设置响应数据的编码
        response.setCharacterEncoding("utf-8");
        //2告诉浏览器响应数据的内容类型以及编码
        response.setContentType("application/json;charset=utf-8");
        //3获取输出流对象
        PrintWriter out=response.getWriter();
        //4 将map转换为json数据
        String result=new ObjectMapper().writeValueAsString(map);
        //5 将数据响应到客户端
        out.println(result);
        out.flush();
    }
}

安全配置类

定义认证规则及异常处理,例如:

package sso.auth.config;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    public BCryptPasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //1.关闭跨域攻击
        http.csrf().disable();
        //2.配置登录url(登录表单使用哪个页面)
        http.formLogin()
         .successHandler(authenticationSuccessHandler())
         .failureHandler(authenticationFailureHandler());
        //设置需要认证与拒绝访问的异常处理器
        http.exceptionHandling()
                .authenticationEntryPoint(authenticationEntryPoint());
        //3.放行登录url(不需要认证就可以访问)
        http.authorizeRequests()
                .anyRequest().authenticated();//除了以上资源必须认证才可访问
    }
    //认证成功处理器
    public AuthenticationSuccessHandler authenticationSuccessHandler(){
        return (httpServletRequest, httpServletResponse,authentication)-> {
            User principal = (User)authentication.getPrincipal();
            Map<String,Object> map=new HashMap<>();
            map.put("state",200);
            map.put("message","Login ok");
            Map<String,Object> jwtMap=new HashMap<>();
            jwtMap.put("username", principal.getUsername());
            List<String> authorities = new ArrayList<>();
            principal.getAuthorities().forEach((authority)-> {
                    authorities.add(authority.getAuthority());
            });
            jwtMap.put("authorities",authorities);
            String token=JwtUtils.generatorToken(jwtMap);
            map.put("token", token);
            WebUtils.writeJsonToClient(httpServletResponse,map);
        };
    }
    //认证失败处理器
    public AuthenticationFailureHandler authenticationFailureHandler(){
        return (httpServletRequest, httpServletResponse, e) -> {
            Map<String,Object> map=new HashMap<>();
            map.put("state",500);
            map.put("msg","username or password error");
            WebUtils.writeJsonToClient(httpServletResponse,map);
        };
    }
    //没有认证时执行DefaultAuthenticationEntryPoint对象
    public AuthenticationEntryPoint authenticationEntryPoint(){
        return (httpServletRequest, httpServletResponse, e)->{
                Map<String,Object> map=new HashMap<>();
                map.put("state",401);//SC_UNAUTHORIZED 的值为401
                map.put("message","请先登录再访问");
                WebUtils.writeJsonToClient(httpServletResponse,map);
        };
    }
}

认证逻辑对象

定义业务对象,处理客户端的登陆请求,例如:

package sso.auth.service;

@Service
public class UserDetailServiceImpl implements UserDetailsService {

    @Autowired
    private BCryptPasswordEncoder passwordEncoder;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            //1.基于用户名从数据库查询用户信息
            //SysUser user=userMapper.selectUserByUsername(username);//查数据库
            if(!"jack".equals(username))//假设这是从数据库查询的信息
                throw new UsernameNotFoundException("user not exists");
            //2.将用户信息封装到UserDetails对象中并返回
            //假设这个密码是从数据库查询出来的
            String encodedPwd=passwordEncoder.encode("123456");
            //假设这个权限信息也是从数据库查询到的
            //List<String> permissions=userMapper.selectUserPermissions(username);//查数据库
            //假如分配权限的方式是角色,编写字符串时用"ROLE_"做前缀
            List<GrantedAuthority> grantedAuthorities =
             AuthorityUtils.commaSeparatedStringToAuthorityList( "sys:res:retrieve,sys:res:create");
            //这个user是SpringSecurity提供的UserDetails接口的实现,用于封装用户信息
            //后续我们也可以基于需要自己构建UserDetails接口的实现
            User user=new User(username,encodedPwd,grantedAuthorities);
            return user;//这里的返回值会交给springsecurity去校验
        }
}

此对象编写好以后,可以启动服务基于postman进行登陆访问测试。

资源服务实现

工具类

第一步:定义JWT工具类,主要用于解析JWT令牌,例如

package sso.resource.util;

public class JwtUtils {
    private static String secret="AAABBBCCCDDDEEE";
    /**基于负载和算法创建token信息*/
    public static String generatorToken(Map<String,Object> map){
        return Jwts.builder()
                .setClaims(map)
                .setExpiration(new Date(System.currentTimeMillis()+30*60*1000))
                .setIssuedAt(new Date())
                .signWith(SignatureAlgorithm.HS256,secret)
                .compact();//签约,创建token
    }
    /**解析token获取数据*/
    public static Claims getClaimsFromToken(String token){
        return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
    }
    /**判定token是否失效*/
    public static boolean isTokenExpired(String token){
        Date expiration=getClaimsFromToken(token).getExpiration();
        return expiration.before(new Date());
    }
}

第二步:定义Web工具类,用于向客户端响应json数据

package sso.auth.util;
public class WebUtils {
    public static void writeJsonToClient(HttpServletResponse response, Map<String,Object> map)
            throws IOException {
        //1设置响应数据的编码
        response.setCharacterEncoding("utf-8");
        //2告诉浏览器响应数据的内容类型以及编码
        response.setContentType("application/json;charset=utf-8");
        //3获取输出流对象
        PrintWriter out=response.getWriter();
        //4 将map转换为json数据
        String result=new ObjectMapper().writeValueAsString(map);
        //5 将数据响应到客户端
        out.println(result);
        out.flush();
    }
}

安全配置类

在资源服务中定义权限配置类,默认将所有认证请求放行,例如:

package sso.resource.config;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
         http.csrf().disable();
         http.exceptionHandling()
                .accessDeniedHandler(accessDeniedHandler());
        http.authorizeRequests().anyRequest().permitAll();
    }
    //没有权限时执行此处理器方法
    public AccessDeniedHandler accessDeniedHandler(){
        return (httpServletRequest, httpServletResponse, e)-> {
                Map<String,Object> map=new HashMap<>();
                map.put("state",403);//SC_FORBIDDEN的值是403
                map.put("message","没有访问权限,请联系管理员");
                WebUtils.writeJsonToClient(httpServletResponse,map);

        };
    }
}

资源服务对象

定义资源服务对象,用于处理客户端的资源访问服务,例如:

package sso.resource.controller;
@RestController
public class ResourceController {
    @PreAuthorize("hasAuthority('sys:res:create')")
    @RequestMapping("/doCreate")
    public String doCreate(HttpServletResponse response){

        return "create resource (insert data) ok";
    }
    /**查询操作*/
    @PreAuthorize("hasAuthority('sys:res:retrieve')")
    @RequestMapping("/doRetrieve")
    public String doRetrieve(){//Retrieve 表示查询
        return "query resource (select data) ok";
    }
    /**修改操作*/
    @PreAuthorize("hasAuthority('sys:res:update')")
    @RequestMapping("/doUpdate")
    public String doUpdate(){
        return "update resource (update data) ok";
    }
    /**删除操作*/
    @PreAuthorize("hasAuthority('sys:res:delete')")
    @RequestMapping("/doDelete")
    public String doDelete(){
        return "delete resource (dalete data) ok";
    }

}

Spring MVC拦截器

资源服务器中的资源不是所有人都可以访问的,需要具备一定权限才可以,首先我们要判定是否登陆,然后判定登陆用户是否有权限,有访问权限才可以授权访问,这个操作可以放到spring mvc拦截器中进行实现,例如:

第一步:定义Spring MVC 拦截器.

package sso.resource.interceptor;

public class TokenInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String token=request.getHeader("token");
        if(token==null||"".equals(token)) throw new RuntimeException("请先登陆");
        if(JwtUtils.isTokenExpired(token)) throw new RuntimeException("请先登陆");
        Claims claims=JwtUtils.getClaimsFromToken(token);
        List<String> list = (List<String>) claims.get("authorities");
        String[]authorities=list.toArray(new String[]{});
        UserDetails userDetails= User.builder()
                .username((String)claims.get("username"))
                .password("")
                .authorities(authorities)
                .build();
        //将UserDetails对象保存到一个可以和Spring-Security交互的对象中
        PreAuthenticatedAuthenticationToken authenticationToken=
                new PreAuthenticatedAuthenticationToken(
                        userDetails,userDetails.getPassword(),
                        AuthorityUtils.createAuthorityList(authorities));
        //将本次解析的用户详情和当前请求关联
        //关联之后才能在后面的控制器中获得用户详情
        authenticationToken.setDetails(new WebAuthenticationDetails(request));
        //将当前用户详情保存到Spring-Security上下文
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        return true;
    }
}

第二步:创建Spring Web配置类,用于注册和配置Spring MVC拦截器,例如:

package sso.resource.config;

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new TokenInterceptor())
                .addPathPatterns("/**");
    }
}

资源访问测试

第一步:启动认证服务器,通过postman进行登陆认证,例如:

在这里插入图片描述
第二步:启动资源服务器,并基于认证服务器返回的令牌进行资源访问
在这里插入图片描述

创建通用工程

背景分析

当多个项目都有一部分公共资源需要重复编写时,我们可以创建一个公共工程,在这个工程中创建共性对象和依赖.其它工程需要时直接引用即可.

创建工程

![在这里插入图片描述](https://img-blog.csdnimg.cn/c4edc36646c14ab98e1089f05d6da896.png

初始化工程

第一步:添加项目依赖

 <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt</artifactId>
        <version>0.9.1</version>
    </dependency>

第二步:拷贝工具类
将sso-auth工程中的WebUtils,JwtUtils拷贝到sso-common工程的sso.util包中

创建跨域配置类

在前后端分离工程中,当通过前端工程访问认证服务和资源服务时,需要进行跨域配置,例如:

package sso.config;

@Configuration
public class CorsFilterConfig {
    /**服务端过滤器层面的跨域设计*/
    @Bean
    public FilterRegistrationBean<CorsFilter> filterFilterRegistrationBean(){
        //1.对此过滤器进行配置(跨域设置-url,method)
        UrlBasedCorsConfigurationSource configSource=new UrlBasedCorsConfigurationSource();
        CorsConfiguration config=new CorsConfiguration();
        config.addAllowedHeader("*");//所有请求头信息
        config.addAllowedMethod("*");//所有请求方式,post,delete,get,put,....
        config.addAllowedOrigin("*");//所有请求参数
        config.setAllowCredentials(true);//所有认证信息,例如cookie
        //2.注册过滤器并设置其优先级
        configSource.registerCorsConfiguration("/**", config);
        FilterRegistrationBean<CorsFilter> fBean=
                new FilterRegistrationBean(
                        new CorsFilter(configSource));
        //设置此过滤器的优先级最高
        fBean.setOrder(Ordered.HIGHEST_PRECEDENCE);
        return fBean;
    }
}

引用通用工程

第一步:删除sso-auth,sso-resource 工程下的util包
第二步:删除sso-auth,sso-resource 工程中的公共依赖
第三步:在sso-auth,sso-resource工程中添加通用工程依赖

 <dependency>
        <groupId>com.cy.jt</groupId>
        <artifactId>sso-common</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>

启动工程测试

打开postman进行重新登录,访问资源测试.

创建前端工程

背景分析

我们做后端,一般在测试时直接基于postman进行访问就可以,为了更好理解前后端通讯过程,我们暂且基于springboot工程构建一个前端工程.

创建前端工程

在这里插入图片描述

初始化工程

第一步:添加web依赖,代码如下:

 <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

第二步:创建application.yml配置文件,代码如下:

server:
  port: 80

第三步:创建启动类,代码如下:

package sso;
@SpringBootApplication
public class UIApplication {
    public static void main(String[] args) {
        SpringApplication.run(UIApplication.class,args);
    }
}

创建静态页面

将课前资料中的static目录直接拷贝到项目中的resource目录下.

登录页面login.html内容如下:

<!doctype html>
<html lang="en">
<head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <!-- Bootstrap CSS -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
    <title>login</title>
</head>
<body>
<div class="container"id="app">
    <h3>Please Login</h3>
    <form>
        <div class="mb-3">
            <label for="usernameId" class="form-label">Username</label>
            <input type="text" v-model="username" class="form-control" id="usernameId" aria-describedby="emailHelp">
        </div>
        <div class="mb-3">
            <label for="passwordId" class="form-label">Password</label>
            <input type="password" v-model="password" class="form-control" id="passwordId">
        </div>
        <button type="button" @click="doLogin()" class="btn btn-primary">Submit</button>
    </form>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script>
    var vm=new Vue({
        el:"#app",//定义监控点,vue底层会基于此监控点在内存中构建dom树
        data:{ //此对象中定义页面上要操作的数据
            username:"",
            password:""
        },
        methods: {//此位置定义所有业务事件处理函数
            doLogin() {
                //1.定义url
                let url = "http://localhost:8081/login"
                //2.定义参数

                var params = new URLSearchParams()
                params.append('username',this.username);
                params.append('password',this.password);
                //3.发送异步请求
                axios.post(url, params).then((response) => {
                   debugger
                   var data=response.data;
                   console.log(data);
                    if (data.state == 200) {
                        alert("login ok");
                        window.localStorage.setItem("token",data.token);
                        location.href="/index.html"
                    } else {
                        alert(response.message);
                    }
                })
            }
        }
    });
</script>
</body>
</html>

登录成功页面index.html,代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<div id="appIndex">
<h1>Index Page <a href="#"  @click="doLogout()">Logout</a></h1>
<h2>CRUD(Create,Retrieve,Update,Delete) Operation</h2>
<ul>
    <li><a href="#" @click="doCreate()">Create(添加-insert)</a></li>
    <li><a href="#">Retrieve(查询-select)</a></li>
    <li><a href="#" @click="doUpdate()">Update(更新-update)</a></li>
    <li><a href="#">Delete(删除-delete)</a></li>
</ul>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script>
    let vm=new Vue({
        el:"#appIndex",//定义监控点,vue底层会基于此监控点在内存中构建dom树
        methods: {//此位置定义所有业务事件处理函数
            doCreate() {
                //1.定义url
                let url = "http://localhost:8091/doCreate"
                //3.发送异步请求
                let token=localStorage.getItem("token");
                axios.get(url,{headers:{"token":token==null?"":token}}).then((response) => {
                   alert(response.data)
                })
            },
            doUpdate() {
                //1.定义url
                let url = "http://localhost:8091/doUpdate"
                //3.发送异步请求
                axios.get(url,{headers:{"token":localStorage.getItem("token")}}).then((response) => {
                    debugger
                    alert(response.data.message);
                })
            },
            doLogout() {
                //移除token
                localStorage.removeItem('token');
                //跳转到登录页面
                location.href="/login.html";
            },
        }
    });
</script>
</body>
</html>

工程访问测试

启动sso-auth,sso-resource,sso-ui 工程,然后访问http://localhost/login.html进行登录
在这里插入图片描述
输入正确的账号,密码执行登录,登录成功以后跳转到如下页面.
在这里插入图片描述
然后,对create,update选项进行访问,检测输出结果.

资源访问过程分析

从登录认证,到资源访问,其过程如下:
在这里插入图片描述

数据库访问操作

背景分析

目前我们登录时的账号,用户权限信息都是写死在UserServiceImpl类中的,实际项目中会从数据库查询用户以及用户对应权限信息.

业务及表设计

实际项目中用户权限控制,通常是通过用户,角色,菜单以及他们的关系表进行数据存储,其业务描述如下:
在这里插入图片描述
其sql脚本如下:

DROP DATABASE IF EXISTS `jt_security`;
CREATE DATABASE  `jt_security` DEFAULT CHARACTER SET utf8mb4;

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

use `jt_security`;

CREATE TABLE `sys_menu` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `name` varchar(50) NOT NULL COMMENT '权限名称',
  `permission` varchar(200) DEFAULT NULL COMMENT '权限标识',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='权限表';
CREATE TABLE `sys_role` (
  `id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT '角色ID',
  `role_name` varchar(50) NOT NULL COMMENT '角色名称',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='角色表';
CREATE TABLE `sys_role_menu` (
  `id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `role_id` bigint(11) DEFAULT NULL COMMENT '角色ID',
  `menu_id` bigint(11) DEFAULT NULL COMMENT '权限ID',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8  COMMENT='角色与权限关系表';

CREATE TABLE `sys_user` (
  `id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT '用户ID',
  `username` varchar(50) NOT NULL COMMENT '用户名',
  `password` varchar(100) DEFAULT NULL COMMENT '密码',
  `status` varchar(10) DEFAULT NULL COMMENT '状态 PROHIBIT:禁用   NORMAL:正常',
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE KEY `username` (`username`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8  COMMENT='系统用户表';

CREATE TABLE `sys_user_role` (
  `id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `user_id` bigint(11) DEFAULT NULL COMMENT '用户ID',
  `role_id` bigint(11) DEFAULT NULL COMMENT '角色ID',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8  COMMENT='用户与角色关系表';

INSERT INTO `sys_menu` VALUES (1, 'select users', 'sys:res:create');
INSERT INTO `sys_menu` VALUES (2, 'select menus', 'sys:res:retrieve');
INSERT INTO `sys_menu` VALUES (3, 'select roles', 'sys:res:delete');
INSERT INTO `sys_role` VALUES (1, 'ADMIN');
INSERT INTO `sys_role` VALUES (2, 'USER');
INSERT INTO `sys_role_menu` VALUES (1, 1, 1);
INSERT INTO `sys_role_menu` VALUES (2, 1, 2);
INSERT INTO `sys_role_menu` VALUES (3, 1, 3);
INSERT INTO `sys_role_menu` VALUES (4, 2, 1);
INSERT INTO `sys_user` VALUES (1,'admin','$2a$10$hIAewJVvpTdDSidROQmoXuBBucjLC7sxf7PDMWggZG49cKYhTXt16','NORMAL');
INSERT INTO `sys_user` VALUES (2,'user','$2a$10$hIAewJVvpTdDSidROQmoXuBBucjLC7sxf7PDMWggZG49cKYhTXt16','NORMAL');
INSERT INTO `sys_user_role` VALUES (1, 1, 1);
INSERT INTO `sys_user_role` VALUES (2, 2, 2);

项目初始化

第一步:在sso-auth工程中添加,如下依赖:

        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.2.0</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.21</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

第二步:修改sso-auth工程中配置文件,添加访问数据库部分,例如:

spring:
  datasource:
    url: jdbc:mysql:///jt_security?serverTimezone=Asia/Shanghai&characterEncoding=utf8
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver #可以省略,默认会自动识别

业务代码编写

第一步:创建UserMapper接口,并定义数据访问方法,代码如下:

package sso.dao;

@Mapper
public interface UserMapper {
    /**
     * 基于用户查询用户信息
     * @param username
     * @return 查询到的用户信息,表中的字段名会作为map中key,字段名对应的值会
     * 作为map中的value进行存储
     */
    @Select("select * from sys_user where username=#{username}")
    Map<String,Object> selectUserByUsername(
            @Param("username")String username);

    /**
     * 基于用户id查询用户权限信息
     */
    @Select(" select distinct m.permission " +
            " from sys_user u left join sys_user_role ur on u.id=ur.user_id " +
            " left join sys_role_menu rm on ur.role_id=rm.role_id " +
            " left join sys_menu m on rm.menu_id=m.id " +
            " where u.id=#{id}")
    List<String> selectUserPermissions(@Param("id") Long id);
}

第二步:修改UserDetailServiceImpl类添加数据库访问操作,例如:

package sso.service;
/**
 * 通过此对象处理登录请求
 */
@Service
public class UserDetailServiceImpl implements UserDetailsService {
    @Autowired
    private BCryptPasswordEncoder passwordEncoder;

    @Autowired
    private UserMapper userMapper;
    @Override
    public UserDetails loadUserByUsername(String username)
            throws UsernameNotFoundException {
        //1.基于用户名查询用户信息
        Map<String,Object> userMap=userMapper.selectUserByUsername(username);
        if(userMap==null)throw new UsernameNotFoundException("user not exists");
        //2.查询用户权限信息并封装查询结果
        List<String> userPermissions=
        userMapper.selectUserPermissions((Long)userMap.get("id"));
        //权限信息后续要从数据库去查
        return new User(username,
                (String)userMap.get("password"),//来自数据库
                AuthorityUtils.createAuthorityList(//来自数据库
                        userPermissions.toArray(new String[]{})));
        //这个值返回给谁?谁调用此方法这个就返回给谁.
    }
}

第三步,启动工程进行访问测试.

总结(summary)

重难点分析

  • 单体架构中的登录设计
  • 分布式架构中的单点登录设计
  • SpringSecutiry在认证服务器和资源服务器中的配置
  • JWT在认证授权系统中的应用

常见FAQ

  • 传统单体架构方式的会话是是如何实现的?(Cookie+Session)
  • 传统单体架构方式的登录在分布式架构中有什么缺陷?(cookie的跨域,session的共享)
  • 分布式架构中的认证方式如何实现?(方式1:Session数据持久化,方式2:认证服务器创建令牌,客户端
    存储令牌,资源服务端解析令牌)
  • 认证服务器用来做什么?(创建并响应令牌,设置认证机制-登录成功,失败,没有认证)
  • 认证服务器的令牌基于什么规范进行创建?(JWT-JSON Web Token)
  • 资源服务器你要做什么?(解析令牌,存储用户认证和权限信息,提供有条件的资源访问)
  • SpringBoot工程中编写单元测试要注意什么?(包-启动类所在包或子包,注解-@SpringBootTest,@Test-org.junit.jupiter.api.Test)
  • SecurityConfig的作用是什么?(配置认证规则,授权方式)
  • UserDetailsService接口的作用是什么?(访问数据库用户信息以及用户对应的权限信息,并进行封装,底层会交给AuthenticationManager管理器去进行认证.)
  • @EnableGlobalMethodSecurity 注解的作用是什么?(描述启动类或配置,用于告诉底层系统,假如方法上有 @PreAuthorize注解,则在方法层面启动权限检测,有权限则授权访问,没有权限则抛异常.)
  • 客户端拿到JWT令牌以后,如何进行的存储?(localStorage)
  • 客户端在访问资源服务时,如何将令牌传递到资源服务器?(将令牌放在ajax请求的请求头中)

BUG分析

  • 401 (访问资源时还没有认证)
  • 403 (访问资源时没有权限)
  • NullPointerException (对象访问属性或方法时因为对象为空而出现的异常)
  • ClassNotFoundException(类没有找到,假如不是自己类,检查对应的依赖.)
  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2021-07-28 00:11:50  更:2021-07-28 00:11:56 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/1 12:41:37-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码