• Spring Security :二【原理解析、会话管理、RBAC中集成认证和授权、JWT】


    三、原理解析

    3.1 结构分析

    Spring Security所解决的问题就是安全访问控制,而安全访问控制功能其实就是对所有进入系统的请求进行拦截,校验每个请求是否能够访问它所期望的资源。根据前边知识的学习,可以通过Filter或AOP等技术来实现,Spring Security对Web资源的保护是靠Filter实现的,所以从这个Filter来入手,逐步深入Spring Security原理。

    当初始化Spring Security时,会创建一个名为 SpringSecurityFilterChain 的Servlet过滤器,类型为 org.springframework.security.web.FilterChainProxy,它实现了javax.servlet.Filter,因此外部的请求会经过此类,下图是Spring Security过虑器链结构图:

    在这里插入图片描述

    Spring Security功能的实现主要是由一系列过滤器链相互配合完成。

    在这里插入图片描述

    下面介绍过滤器链中主要的几个过滤器及其作用:

    SecurityContextPersistenceFilter 这个Filter是整个拦截过程的入口和出口(也就是第一个和最后一个拦截器),会在请求开始时从配置好SecurityContextRepository 中获取 SecurityContext,然后把它设置给 SecurityContextHolder。在请求完成后将 SecurityContextHolder 持有的 SecurityContext 再保存到配置好 的 SecurityContextRepository,同时清除 securityContextHolder 所持有的 SecurityContext;

    UsernamePasswordAuthenticationFilter 用于处理来自表单提交的认证。该表单必须提供对应的用户名和密 码,其内部还有登录成功或失败后进行处理的 AuthenticationSuccessHandler 和AuthenticationFailureHandler,这些都可以根据需求做相关改变;

    FilterSecurityInterceptor 是用于保护web资源的,使用AccessDecisionManager对当前用户进行授权访问;

    ExceptionTranslationFilter 能够捕获来自 FilterChain 所有的异常,并进行处理。但是它只会处理两类异常: AuthenticationException 和 AccessDeniedException,其它的异常它会继续抛出。

    3.1 登录认证流程分析

    在这里插入图片描述

    让我们仔细分析认证过程:

    1. 用户提交用户名、密码被SecurityFilterChain中的 UsernamePasswordAuthenticationFilter 过滤器获取到, 封装为请求Authentication,通常情况下是UsernamePasswordAuthenticationToken这个实现类。

    2. 然后过滤器将Authentication提交至认证管理器(AuthenticationManager)进行认证

    3. 认证成功后, AuthenticationManager 身份管理器返回一个被填充满了信息的(包括上面提到的权限信息,身份信息,细节信息,但密码通常会被移除) Authentication 实例。

    4. SecurityContextHolder 安全上下文容器将第3步填充了信息的 Authentication ,通过 SecurityContextHolder.getContext().setAuthentication(…)方法,设置到其中。 可以看出AuthenticationManager接口(认证管理器)是认证相关的核心接口,也是发起认证的出发点,它 的实现类为ProviderManager。而Spring Security支持多种认证方式,因此ProviderManager维护着一个 List 列表,存放多种认证方式,最终实际的认证工作是由 AuthenticationProvider完成的。咱们知道web表单的对应的AuthenticationProvider实现类为 DaoAuthenticationProvider,它的内部又维护着一个UserDetailsService负责UserDetails的获取。最终 AuthenticationProvider将UserDetails填充至Authentication。 认证核心组件的大体关系如下:

    流程图简易图:

    在这里插入图片描述

    3.1.1 UserDetailsService

    刚才我们分析流程中看到DaoAuthenticationProvider去调用UserDetailsService 去查询数据然后进行对比, 这个UserDetailsService在整个认证流程中的作用只负责查数据, 具体是查询内存的数据还是数据库的数据又我们配置自己决定, 对比的操作是DaoAuthenticationProvider内部在做。

    public interface UserDetailsService { 
    
      	UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; 
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    3.1.2 自定义UserDetailsService

    原来配置:

     @Bean
        public UserDetailsService userDetailsService() {
            InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
            manager.createUser(User.withUsername("zhangsan").password("123").authorities("p1").build());
            manager.createUser(User.withUsername("lisi").password("456").authorities("p2").build());
            return manager;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    之前我们的配置都是在内存中查询数据, 但是在实际项目开发中都是查询数据库。

    自定义 UserDetailsService 操作

    @Service
    public class SpringDataUserDetailsService implements UserDetailsService {
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            //登录账号 
            System.out.println("username=" + username);
            // 根据账号去数据库查询... 
            // 这里暂时使用静态数据 
            UserDetails userDetails = User.withUsername(username).password("123").
                    authorities("p1").build();
            return userDetails;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    重启工程,请求认证,SpringDataUserDetailsService的loadUserByUsername方法被调用 ,查询用户信息。

    3.1.3 PasswordEncoder

    认识PasswordEncoder :

    DaoAuthenticationProvider认证处理器通过UserDetailsService获取到UserDetails后,它是如何与请求

    Authentication中的密码做对比呢?

    在这里插入图片描述

    在这里Spring Security为了适应多种多样的加密类型,又做了抽象,DaoAuthenticationProvider通过 PasswordEncoder接口的matches方法进行密码的对比,而具体的密码对比细节取决于实现:

    public interface PasswordEncoder {
        String encode(CharSequence var1);
    
        boolean matches(CharSequence var1, String var2);
    
        default boolean upgradeEncoding(String encodedPassword) {
            return false;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    而Spring Security提供很多内置的PasswordEncoder,能够开箱即用,使用某种PasswordEncoder只需要进行如

    下声明即可,如下:

    @Bean 
    public PasswordEncoder passwordEncoder() { 
      return NoOpPasswordEncoder.getInstance(); 
    } 
    
    • 1
    • 2
    • 3
    • 4

    NoOpPasswordEncoder采用字符串匹配方法,不对密码进行加密比较处理。

    实际项目中推荐使用BCryptPasswordEncoder, Pbkdf2PasswordEncoder, SCryptPasswordEncoder等,感兴趣的大家可以看看这些PasswordEncoder的具体实现。

    在安全配置类中定义:

    @Bean 
    public PasswordEncoder passwordEncoder() { 
      return new BCryptPasswordEncoder(); 
    } 
    
    • 1
    • 2
    • 3
    • 4

    测试发现: Encoded password does not look like BCrypt
    原因: 数据库中的密码是明文的, 前台传过来的密码加密完以后进行对比,不一致。

    使用BCrypt对于密码进行加密

    1、对于密码进行机密和验证操作

        @org.junit.Test
        public void test(){
            String gensalt = BCrypt.gensalt();
            System.out.println(gensalt);
            String password = BCrypt.hashpw("123",gensalt );
            System.out.println(password);
    
            boolean checkpw = BCrypt.checkpw("123", "$2a$10$XeDXzobQ32ExDoZ1XNh1DOvAxJFtZgwwM1njc.vOzeYRFHyYPv1ay");
            System.out.println(checkpw);
    
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    2、 修改配置类中的密码格式:

    UserDetails userDetails = User.withUsername(username).password("$2a$10$m44lS0/w2yRIuFMzUIRJ9OFUq9HMaLm2eqkSlKdfASpyZJgYrGe2.").
                    authorities("p1").build();
    
    • 1
    • 2

    注: 实际项目中存储的密码就是密文的。

    3.2 授权流程分析

    3.2.1 配置方式的原理解析

    流程图:

    通过快速上手我们知道,Spring Security可以通过 http.authorizeRequests() 对web请求进行授权保护。Spring Security使用标准Filter建立了对web请求的拦截,最终实现对资源的授权访问。

    在这里插入图片描述

    分析授权流程:

    拦截请求,已认证用户访问受保护的web资源将被SecurityFilterChain中的 FilterSecurityInterceptor 的子类拦截。

    获取资源访问策略,FilterSecurityInterceptor会从 SecurityMetadataSource 的子类 DefaultFilterInvocationSecurityMetadataSource 获取要访问当前资源所需要的权限 Collection 。

    SecurityMetadataSource其实就是读取访问策略的抽象,而读取的内容,其实就是我们配置的访问规则, 读取访问策略如:

    http.authorizeRequests() .antMatchers("/r/r1").hasAuthority("p1") .antMatchers("/r/r2").hasAuthority("p2") ...
    
    • 1

    最后,FilterSecurityInterceptor会调用 AccessDecisionManager 进行授权决策,若决策通过,则允许访问资源,否则将禁止访问。

    在这里插入图片描述

    3.2.2 注解方式原理解析

    基于方法的授权采用 Aop 进行实现.

    流程分析图:

    在这里插入图片描述

    四、会话管理

    4.1 获取用户身份

    用户认证通过后,为了避免用户的每次操作都进行认证可将用户的信息保存在会话中。spring security提供会话管理,认证通过后将身份信息放入SecurityContextHolder上下文,SecurityContext与当前线程进行绑定,方便获取 用户身份。

    编写方法:

    @RequestMapping("/getUsername")
        public String getUsername(){
            Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
            Object principal = authentication.getPrincipal();
            String username = "";
            if(principal instanceof UserDetails){
                username = ((UserDetails) principal).getUsername();
            }else{
                username=  principal.toString();
            }
            return username;
        } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    4.2 会话控制

    我们可以通过以下选项准确控制会话何时创建以及Spring Security如何与之交互:

    机制描述
    always如果没有session存在就创建一个
    ifRequired如果需要就创建一个Session(默认)登录时
    neverSpringSecurity 将不会创建Session,但是如果应 用中其他地方创建了Session,那么Spring Security将会使用它。
    statelessSpringSecurity将绝对不会创建Session,也不使用Session

    通过以下配置方式对该选项进行配置:

    .and()            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    
    • 1

    默认情况下,Spring Security会为每个登录成功的用户会新建一个Session,就是ifRequired

    若选用never,则指示Spring Security对登录成功的用户不创建Session了,但若你的应用程序在某地方新建了session,那么Spring Security会用它的。

    若使用stateless,则说明Spring Security对登录成功的用户不会创建Session了,你的应用程序也不会允许新建session。并且它会暗示不使用cookie,所以每个请求都需要重新进行身份验证。这种无状态架构适用于REST API 及其无状态认证机制。

    五、 RBAC中集成认证和授权

    5.1 集成认证

    ####5.1.1 导入依赖

    <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-securityartifactId>
    dependency>
    
    • 1
    • 2
    • 3
    • 4

    启动项目,发现前端界面在访问的时候出现了跨域问题。

    原因: 因为跨域会发送一个预请求看服务端是否支持跨域, 但是这个预请求也会被拦截,之前我们在在拦截器中判断是否是 handlerMethod 决定是否放行,但是现在我们用的是 SpringSecurity ,是让 SpringSecurity 给拦截了。

    5.1.2 进行配置规则
    @Override
        protected void configure(HttpSecurity http) throws Exception {
    
            //进制 crsf
            http.csrf().disable();
            //配置拦截规则
            http.authorizeRequests().
                    antMatchers("/api/code","/api/login","/api/logout").
                    permitAll().
                    anyRequest().
                    authenticated();
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    重启访问: 验证码已经可以出来了, 但是点击登录调用 login 还是之前我们自己写的 login 方法,我们要让 SpringSecurity帮我们做认证,注释之前在 LoginServiceImpl 实现类中登录的代码。

    5.1.3 自定义 UserDetailService
    @Service
    public class UserDetailServiceImpl implements UserDetailsService {
        @Autowired
        private EmployeeMapper employeeMapper;
    
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            //根据用户名去查询数据
            if(StringUtils.isEmpty(username)){
                return null;
            }
            Employee employee =  employeeMapper.selectByUsername(username);
            return User.withUsername(employee.getUsername()).password(employee.getPassword()).authorities("p1").build();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    因为我们的数据是保存在数据库中的所以操作的时候需要自定义 UserDetailService,去查询数据库数据,但是新的问题出现了我们定义的这个类不会被调用。

    思考:为什么不会调用我们的UserDetailService, 之前在学习的时候可以被调用。

    原因: 之前我们用的表单提交的方式,直接用了他表单处理的 filter,但是现在我们前端那边是用的 ajax 提交不是表单提交,他的表达提交 filter 处理不了,需要我们自己来处理。

    5.1.4 加入认证器
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
    
     @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(userDetailService).passwordEncoder(passwordEncoder());
        }
    
        @Bean
        public PasswordEncoder passwordEncoder(){
            return NoOpPasswordEncoder.getInstance();
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    5.1.5 在 loginService中调用认证器进行认证
         
            UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(loginVO.getUsername(),loginVO.getPassword());
            Authentication authenticate =
                    authenticationManager.authenticate(token);
            User user = (User) authenticate.getPrincipal();
    
    • 1
    • 2
    • 3
    • 4
    • 5

    这里遇到了新的问题,发现返回的是 User,但是我们要把 Employe 对象放到 redis 中 , user 中只有当前登录用户的账号密码和权限信息

    5.1.6 自定义 User
    @Getter
    @Setter
    public class  LoginUser implements UserDetails {
        private Employee employee;
        @Override
        public Collection<? extends GrantedAuthority> getAuthorities() {
            return null;
        }
    
        @Override
        public String getPassword() {
            return employee.getPassword();
        }
    
        @Override
        public String getUsername() {
            return employee.getUsername();
        }
    
        /**
         * 账户是否未过期,过期无法验证
         */
        @Override
        public boolean isAccountNonExpired() {
            return true;
        }
    
        /**
         * 指定用户是否解锁,锁定的用户无法进行身份验证
         *
         * @return
         */
        @Override
        public boolean isAccountNonLocked() {
            return true;
        }
    
        /**
         * 指示是否已过期的用户的凭据(密码),过期的凭据防止认证
         *
         * @return
         */
        @Override
        public boolean isCredentialsNonExpired() {
            return true;
        }
    
        /**
         * 是否可用 ,禁用的用户不能身份验证
         *
         * @return
         */
        @Override
        public boolean isEnabled() {
            return true;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57

    在 UserDetailServiceImpl 中

    @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            //根据用户名去查询数据
            if(StringUtils.isEmpty(username)){
                return null;
            }
            Employee employee =  employeeMapper.selectByUsername(username);
            LoginUser loginUser = new LoginUser();
            loginUser.setEmployee(employee);
            return loginUser;
    //        return User.withUsername(employee.getUsername()).password(employee.getPassword()).authorities("p1").build();
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    loginServiceImpl 中

    LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
    Employee employee = loginUser.getEmployee();
    
    • 1
    • 2

    现在登录已经可以登录进去了, 但是发现访问部门管理这些资源,出现了如下问题,是又出现跨域问题了吗? 我们不是已经解决跨域问题了吗。

    在这里插入图片描述

    原因:我们匹配的规则是除了"/api/code",“/api/login”,“/api/logout” 都需要进行拦截判断是否认证,在 SpringSecurity 中会从SecurityContextHolder.getContext().getAuthentication()去拿当前用户信息,看是否登录的。

    5.1.8 自定义 filter
    @Component
    public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
        @Autowired
        private RedisUtils redisUtils;
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
            String userId = request.getHeader("userId");
           String objJson = redisUtils.get(Constant.LOGIN_EMPLOYEE + userId);
            if(!StringUtils.isEmpty(objJson)){
               
                Employee employee = JSON.parseObject(objJson, Employee.class);
                UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(employee.getUsername(),employee.getPassword());
                SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
            }
            filterChain.doFilter(request,response);
    
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    加入配置:

      http.addFilterBefore(authenticationTokenFilter,
                    UsernamePasswordAuthenticationFilter.class);
      http.addFilterBefore(corsFilter,
                    JwtAuthenticationTokenFilter.class);
    
    • 1
    • 2
    • 3
    • 4

    5.2 集成授权

    5.2.1 查询授权信息
    public Collection<? extends GrantedAuthority> getAuthorities() {
        // 先查询出来当前用户是否是超级管理员
        PermissionMapper permissionMapper = SpringUtils.getBean(PermissionMapper.class);
        List<GrantedAuthority> list = new ArrayList<>();
        if(employee.isAdmin()){
            // 如果是分配所有权限
            List<Permission> permissions = permissionMapper.selectAll();
            // 如果不是分配用户所拥有的权限
            for (Permission permission : permissions) {
                list.add(new SimpleGrantedAuthority(permission.getExpression()));
            }
        }else{
            //根据用户id 查询用户所拥有权限结合
            List<String> expressions = permissionMapper.queryPermissionByEmpId(employee.getId());
            for (String expression : expressions) {
                list.add(new SimpleGrantedAuthority(expression));
            }
        }
        return list;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    5.2.2 在 filter 中加入权限
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String userId = request.getHeader("userId");
        if(!StringUtils.isEmpty(userId)){
    
            String objJson = redisUtils.get(Constant.LOGIN_EMPLOYEE + userId);
    
            Employee employee = JSON.parseObject(objJson, Employee.class);
            LoginUser loginUser = new LoginUser();
            loginUser.setEmployee(employee);
    
            UsernamePasswordAuthenticationToken token =
                    new UsernamePasswordAuthenticationToken(employee.getUsername(),employee.getPassword(),loginUser.getAuthorities());
            SecurityContextHolder.getContext().setAuthentication(token);
        }
    
        filterChain.doFilter(request,response);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    5.2.3 开启注解支持

    启动类上贴注解: @EnableGlobalMethodSecurity(prePostEnabled = true)

    @SpringBootApplication
    @MapperScan(basePackages = "cn.wolfcode.mapper")
    @EnableGlobalMethodSecurity(prePostEnabled = true)
    public class App {
    
        public static void main(String[] args) {
            SpringApplication.run(App.class,args);
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    方法贴注解: @PreAuthorize(“hasAuthority(‘role:queryByRoleId’)”)

    5.2.4 解决加载权限失败问题

    原因: 由于我们注解权限拦截的原理是采用 Aop ,会对Controller 进行增强,我们注解通过代理类去拿方法是获取不到的

    解决:

    //3 从 Controller 中拿到所有的方法
    Method[] methods = controller.getClass().getSuperclass().getDeclaredMethods();
    
    • 1
    • 2

    六、JWT

    6.1 JWT 介绍

    jsonwebtoken(JWT)是一个开放标准(rfc7519),它定义了一种紧凑的、自包含的方式,用于在各方之间以JSON对象安全地传输信息。此信息可以验证和信任,因为它是数字签名的。jwt可以使用秘密(使用HMAC算法)或使用RSA或ECDSA的公钥/私钥对进行签名

    通俗的讲: JWT简称JSON Web Token,也就是通过JSON形式作为Web应用中的令牌,用于在各方之间安全地将信息作为JSON对象传输。在数据传输过程中还可以完成数据加密、签名等相关处理。

    6.2 JWT 能够做什么

    1、 授权

    这是使用JWT的最常见方案。一旦用户登录,每个后续请求将包括JWT,从而允许用户访问该令牌允许的路由,服务和资源。单点登录是当今广泛使用JWT的一项功能,因为它的开销很小并且可以在不同的域中轻松使用。

    2 、信息交换

    JSON Web Token是在各方之间安全地传输信息的好方法。因为可以对JWT进行签名(例如,使用公钥/私钥对),所以您可以确保发件人是他们所说的人。此外,由于签名是使用标头和有效负载计算的,因此您还可以验证内容是否遭到篡改。

    7.3 为什么使用 JWT

    基于传统的Session认证

    在这里插入图片描述

    缺陷:

    1.每个用户经过我们的应用认证之后,我们的应用都要在服务端做一次记录,以方便用户下次请求的鉴别,通常而言session都是保存在内存中,而随着认证用户的增多,服务端的开销会明显增大

    2因为是基于cookie来进行用户识别的, cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击。

    基于JWT认证

    在这里插入图片描述

    jwt的优势:

    简洁(Compact): 可以通过URL,POST参数或者在HTTP header发送,因为数据量小,传输速度也很快

    自包含(Self-contained):负载中包含了所有用户所需要的信息,避免了多次查询数据库

    因为Token是以JSON加密的形式保存在客户端的,所以JWT是跨语言的,原则上任何web形式都支持。

    6.3 JWT 结构介绍

    6.3.1 令牌组成

    1.标头(Header)
    2.有效载荷(Payload)
    3.签名(Signature)

    因此,JWT通常如下所示:xxxxx.yyyyy.zzzzz Header.Payload.Signature

    7.2.2 Header部分

    标头通常由两部分组成:令牌的类型(即JWT)和所使用的签名算法,例如HMAC SHA256或RSA。它会使用 Base64 编码组成 JWT 结构的第一部分。

    注意:Base64是一种编码,也就是说,它是可以被翻译回原来的样子来的。它并不是一种加密过程。

    {
      "alg": "HS256",
      "typ": "JWT"
    }
    
    • 1
    • 2
    • 3
    • 4

    6.3.2 Payload 部分

    令牌的第二部分是有效负载,其中包含声明。声明是有关实体(通常是用户)和其他数据的声明。同样的,它会使用 Base64 编码组成 JWT 结构的第二部分

    {
      "sub": "1234567890",
      "name": "John Doe",
      "admin": true
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    6.3.3 Signature部分

    前面两部分都是使用 Base64 进行编码的,即前端可以解开知道里面的信息。Signature 需要使用编码后的 header 和 payload 以及我们提供的一个密钥,然后使用 header 中指定的签名算法(HS256)进行签名。签名的作用是保证 JWT 没有被篡改过

    如:
    HMACSHA256(base64UrlEncode(header) + “.” + base64UrlEncode(payload),secret);

    签名目的

    最后一步签名的过程,实际上是对头部以及负载内容进行签名,防止内容被窜改。如果有人对头部以及负载的内容解码之后进行修改,再进行编码,最后加上之前的签名组合形成新的JWT的话,那么服务器端会判断出新的头部和负载形成的签名和JWT附带上的签名是不一样的。如果要对新的头部和负载进行签名,在不知道服务器加密时用的密钥的话,得出来的签名也是不一样的。

    在这里插入图片描述

    6.4 JWT 使用

    6.4.1 引入依赖
    
    <dependency>
      <groupId>com.auth0groupId>
      <artifactId>java-jwtartifactId>
      <version>3.4.0version>
    dependency>
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    6.4.2 生成 token
    //生成令牌
    String token = JWT.create()
      .withClaim("username", "张三")//设置自定义用户名
      .sign(Algorithm.HMAC256("token!Q2W#E$RW"));//设置签名 保密 复杂
    //输出令牌
    System.out.println(token);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    生成结果:

    eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOlsicGhvbmUiLCIxNDMyMzIzNDEzNCJdLCJleHAiOjE1OTU3Mzk0NDIsInVzZXJuYW1lIjoi5byg5LiJIn0.aHmE3RNqvAjFr_dvyn_sD2VJ46P7EGiS5OBMO_TI5jg
    
    • 1
    6.4.3 根据令牌解析数据
    JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("token!Q2W#E$RW")).build();
    DecodedJWT decodedJWT = jwtVerifier.verify(token);
    System.out.println("用户名: " + decodedJWT.getClaim("username").asString());
    
    • 1
    • 2
    • 3
    6.4.4 常见异常
    - SignatureVerificationException:				签名不一致异常
    - TokenExpiredException:    						令牌过期异常
    - AlgorithmMismatchException:						算法不匹配异常
    - InvalidClaimException:								失效的payload异常
    
    • 1
    • 2
    • 3
    • 4
    6.4.6 RBAC 中集成 JWT
    6.4.6.1 抽取工具类
    package cn.wolfcode.util;
    
    /**
     * create By  fjl
     */
    @Component
    @Getter
    @Setter
    public class JWTUtils {
    
        @Value("${jwt.scret}")
        public  String scret;
        @Value("${jwt.head}")
        public  String head;
      
        public  String createTokenMap(Map<String,String> map) {
    
            JWTCreator.Builder builder = JWT.create();
            for (Map.Entry<String, String> entry : map.entrySet())     {
                builder.withClaim(entry.getKey(), entry.getValue());
            }
            String token = builder.sign(Algorithm.HMAC256(scret));
            return token;
        }
        public  String createToken(String key , String value) {
    
            JWTCreator.Builder builder = JWT.create();
            builder.withClaim(key,value);
            String token = builder.sign(Algorithm.HMAC256(scret));
            return token;
        }
    
       s
        public  String getToken1(String token,String key){
    
            //先验证签名
            JWTVerifier verifier = JWT.require(Algorithm.HMAC256(scret)).build();
            //验证其他信息
            DecodedJWT verify = verifier.verify(token);
            String value = verify.getClaim(key).asString();
            return value;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    6.4.6.2 加配置
    jwt:
      scret: abced
      head: Authencation
    
    • 1
    • 2
    • 3
    6.4.6.3 修改 LoginServiceImpl
      @Override
        public String login(LoginVO loginVO) {
            //参数校验
            if(loginVO==null){
                throw new BusinessException("非法操作");
            }
    
            if(StringUtils.isEmpty(loginVO.getUsername()) || StringUtils.isEmpty(loginVO.getPassword())){
                throw new BusinessException("账号密码不能为空");
            }
    
            if(StringUtils.isEmpty(loginVO.getCode())){
                throw new BusinessException("验证码不能为空");
            }
            // 从 redis 中获取密码
            String redisCode = redisUtils.get(Constant.VERFI_CODE_PREFIX + loginVO.getUuid());
            boolean flag = VerifyCodeUtil.verification(redisCode, loginVO.getCode(), true);
            if(!flag){
                throw new BusinessException("验证码不正确");
            }
            // 根据账号密码去查询数据
    //        Employee employee = employeeService.login(loginVO.getUsername(),loginVO.getPassword());
    //        if(employee == null){
    //            throw new BusinessException("账号密码错误");
    //        }
            UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(loginVO.getUsername(),loginVO.getPassword());
            Authentication authenticate =
                    authenticationManager.authenticate(token);
            LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
            Employee employee = loginUser.getEmployee();
    
            //创建 token   login_user:uuid
            String uuid = UUID.randomUUID().toString();
            String jwtToken = jwtUtils.createToken1(Constant.JWT_TOKEN_KEY, uuid);
    
    //         把当前登录用户放到 redis 中为了后去判断是否登录做铺垫
    //         login_employee:id     employee
            redisUtils.set(Constant.LOGIN_EMPLOYEE+uuid, JSON.toJSONString(employee),Constant.EXPRE_TIME);
    //         把当前登录用户所拥有的权限放到 session 中
    //         根据当前用户查询 用户拥有权限表达式
            List<String> expressions = permissionService.queryPermissionByEmpId(employee.getId());
            redisUtils.set(Constant.EMPLOYEE_EXPRESSIONS+uuid,JSON.toJSONString(expressions),Constant.EXPRE_TIME);
            return jwtToken;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    6.4.6.4 修改 JwtAuthenticationTokenFilter
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
    
        String token = request.getHeader(jwtUtils.getHead());
    
        if (!StringUtils.isEmpty(token)) {
            String uuid = jwtUtils.getToken1(token, Constant.JWT_TOKEN_KEY);
            String objJson = redisUtils.get(Constant.LOGIN_EMPLOYEE + uuid);
    
            if(!StringUtils.isEmpty(objJson)){
                Employee employee = JSON.parseObject(objJson, Employee.class);
                LoginUser loginUser = new LoginUser();
                loginUser.setEmployee(employee);
    
                UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(employee.getUsername(), employee.getPassword(), loginUser.getAuthorities());
                SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
            }
        }
        filterChain.doFilter(request, response);
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    6.4.6.5 修改前端main.js
    // 请求拦截
    axios.interceptors.request.use(function(request){
          const token = window.sessionStorage.getItem("token");
          if(token){
            request.headers.Authencation=token;
          }
          return request;
    },function(err){
      return Promise.reject(err)
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    6.4.6.6 修改前端 login.js
     async login() {
          const { data: res } = await this.$http.post("login", this.loginForm);
          console.log(res);
          if (res.code != 200) {
            console.log("登录失败");
          } else {
            console.log("登录成功");
            window.sessionStorage.setItem("token", res.data);
            this.$router.push("/main");
          }
        },
       }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    6.5.6.7 修改路由 index.js
    router.beforeEach((to,from,next) =>{
    	console.log("router---beforeEach")
    	// to 将要访问的路径
    	// from 代表从哪个路径跳转而来
    	// next 是一个函数,表示放行
    	//     next()  放行    next('/login')  强制跳转
    	if(to.path==="/login") return next();
    	const token=window.sessionStorage.getItem("token");
      console.log(token)
    	if(token) return next();
    	next("/login")
    });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    七、附录:HttpSecurity 配置项

    方法说明
    openidLogin()用于基于 OpenId 的验证
    headers()将安全标头添加到响应
    cors()配置跨域资源共享( CORS )
    sessionManagement()允许配置会话管理
    portMapper()向到 HTTPS 或者从 HTTPS 重定向到 HTTP。默认情况下,Spring Security使用一个PortMapperImpl映射 HTTP 端口8080到 HTTPS 端口8443,HTTP 端口80到 HTTPS 端口443
    jee()配置基于容器的预认证。 在这种情况下,认证由Servlet容器管理
    x509()配置基于x509的认证
    rememberMe允许配置“记住我”的验证
    authorizeRequests()允许基于使用HttpServletRequest限制访问
    requestCache()允许配置请求缓存
    exceptionHandling()允许配置错误处理
    securityContext()在HttpServletRequests之间的SecurityContextHolder上设置SecurityContext的管理。 当使用WebSecurityConfifigurerAdapter时,这将
    servletApi()将HttpServletRequest方法与在其上找到的值集成到SecurityContext中。 当使用WebSecurityConfifigurerAdapter时,这将自动应用
    csrf()添加 CSRF 支持,使用WebSecurityConfifigurerAdapter时,默认启用
    logout()添加退出登录支持。当使用WebSecurityConfifigurerAdapter时,这将自动应用。默认情况是,访问URL”/ logout”,使HTTP Session无效来
    anonymous()允许配置匿名用户的表示方法。 当与WebSecurityConfifigurerAdapter结合使用时,这将自动应用。 默认情况下,匿名用户将使用
    formLogin()指定支持基于表单的身份验证。如果未指定FormLoginConfifigurer#loginPage(String),则将生成默认登录页面
    oauth2Login()根据外部OAuth 2.0或OpenID Connect 1.0提供程序配置身份验证
    requiresChannel()配置通道安全。为了使该配置有用,必须提供至少一个到所需信道的映射
    httpBasic()配置 Http Basic 验证
    addFilterAt()允许配置错误处理
    exceptionHandling()在指定的Filter类的位置添加过滤器
  • 相关阅读:
    【残差网络 论文泛读】……DenseNet……(Densely Connected Convolutional Networks)
    探讨代理IP与Socks5代理在跨界电商中的网络安全应用
    centos 安装 docker
    接口获取数据,转成JSONOBJECT
    antd Carousel 重写dot样式
    设计模式学习(十三):观察者模式
    实习报告1——人脸三维重建方法
    基于QWebEngine实现无头浏览器
    BGP协议下的路由聚合、路由反射器、联邦的具体配置与运用(详解)
    网站SEO效果分析
  • 原文地址:https://blog.csdn.net/m0_52896752/article/details/132904915