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

文章目录

    • 三、原理解析
      • 3.1 结构分析
      • 3.1 登录认证流程分析
        • 3.1.1 **UserDetailsService**
        • 3.1.2 自定义UserDetailsService
        • 3.1.3 **PasswordEncoder**
      • 3.2 授权流程分析
        • 3.2.1 配置方式的原理解析
        • 3.2.2 注解方式原理解析
    • 四、会话管理
      • 4.1 获取用户身份
      • 4.2 会话控制
    • 五、 RBAC中集成认证和授权
      • 5.1 集成认证
        • 5.1.2 进行配置规则
        • 5.1.3 自定义 UserDetailService
        • 5.1.4 加入认证器
        • 5.1.5 在 loginService中调用认证器进行认证
        • 5.1.6 自定义 User
        • 5.1.8 自定义 filter
      • 5.2 集成授权
        • 5.2.1 查询授权信息
        • 5.2.2 在 filter 中加入权限
        • 5.2.3 开启注解支持
        • 5.2.4 解决加载权限失败问题
    • 六、JWT
      • 6.1 JWT 介绍
      • 6.2 JWT 能够做什么
      • 6.3 JWT 结构介绍
        • **6.3.1 令牌组成**
        • 6.3.3 Signature部分
      • 6.4 JWT 使用
        • 6.4.1 引入依赖
        • 6.4.2 生成 token
        • 6.4.3 根据令牌解析数据
        • 6.4.4 常见异常
        • 6.4.6 RBAC 中集成 JWT
          • 6.4.6.1 抽取工具类
          • 6.4.6.2 加配置
          • 6.4.6.3 修改 LoginServiceImpl
          • 6.4.6.4 修改 JwtAuthenticationTokenFilter
          • 6.4.6.5 修改前端main.js
          • 6.4.6.6 修改前端 login.js
          • 6.5.6.7 修改路由 index.js
    • 七、附录:HttpSecurity 配置项

三、原理解析

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 :二【原理解析、会话管理、RBAC中集成认证和授权、JWT】_第1张图片

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

Spring Security :二【原理解析、会话管理、RBAC中集成认证和授权、JWT】_第2张图片

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

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

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

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

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

3.1 登录认证流程分析

Spring Security :二【原理解析、会话管理、RBAC中集成认证和授权、JWT】_第3张图片

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

  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。 认证核心组件的大体关系如下:

流程图简易图:

Spring Security :二【原理解析、会话管理、RBAC中集成认证和授权、JWT】_第4张图片

3.1.1 UserDetailsService

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

public interface UserDetailsService { 

  	UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; 

}
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;
    }

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

自定义 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;
    }
}

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

3.1.3 PasswordEncoder

认识PasswordEncoder :

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

Authentication中的密码做对比呢?

Spring Security :二【原理解析、会话管理、RBAC中集成认证和授权、JWT】_第5张图片

在这里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;
    }
}

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

下声明即可,如下:

@Bean 
public PasswordEncoder passwordEncoder() { 
  return NoOpPasswordEncoder.getInstance(); 
} 

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

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

在安全配置类中定义:

@Bean 
public PasswordEncoder passwordEncoder() { 
  return new BCryptPasswordEncoder(); 
} 

测试发现: 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);

    }

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

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

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

3.2 授权流程分析

3.2.1 配置方式的原理解析

流程图:

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

Spring Security :二【原理解析、会话管理、RBAC中集成认证和授权、JWT】_第6张图片

分析授权流程:

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

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

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

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

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

Spring Security :二【原理解析、会话管理、RBAC中集成认证和授权、JWT】_第7张图片

3.2.2 注解方式原理解析

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

流程分析图:

Spring Security :二【原理解析、会话管理、RBAC中集成认证和授权、JWT】_第8张图片

四、会话管理

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;
    } 

4.2 会话控制

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

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

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

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

默认情况下,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>

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

原因: 因为跨域会发送一个预请求看服务端是否支持跨域, 但是这个预请求也会被拦截,之前我们在在拦截器中判断是否是 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();
    }

重启访问: 验证码已经可以出来了, 但是点击登录调用 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();
    }
}

因为我们的数据是保存在数据库中的所以操作的时候需要自定义 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();
    }
5.1.5 在 loginService中调用认证器进行认证
     
        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(loginVO.getUsername(),loginVO.getPassword());
        Authentication authenticate =
                authenticationManager.authenticate(token);
        User user = (User) authenticate.getPrincipal();

这里遇到了新的问题,发现返回的是 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;
    }
}

在 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();
    }

loginServiceImpl 中

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

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

在这里插入图片描述

原因:我们匹配的规则是除了"/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);

    }
}

加入配置:

  http.addFilterBefore(authenticationTokenFilter,
                UsernamePasswordAuthenticationFilter.class);
  http.addFilterBefore(corsFilter,
                JwtAuthenticationTokenFilter.class);

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;
}
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);
}
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);
    }
}

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

5.2.4 解决加载权限失败问题

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

解决:

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

六、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认证

Spring Security :二【原理解析、会话管理、RBAC中集成认证和授权、JWT】_第9张图片

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"
}

6.3.2 Payload 部分

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

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}
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>

6.4.2 生成 token
//生成令牌
String token = JWT.create()
  .withClaim("username", "张三")//设置自定义用户名
  .sign(Algorithm.HMAC256("token!Q2W#E$RW"));//设置签名 保密 复杂
//输出令牌
System.out.println(token);

生成结果:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOlsicGhvbmUiLCIxNDMyMzIzNDEzNCJdLCJleHAiOjE1OTU3Mzk0NDIsInVzZXJuYW1lIjoi5byg5LiJIn0.aHmE3RNqvAjFr_dvyn_sD2VJ46P7EGiS5OBMO_TI5jg
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());
6.4.4 常见异常
- SignatureVerificationException:				签名不一致异常
- TokenExpiredException:    						令牌过期异常
- AlgorithmMismatchException:						算法不匹配异常
- InvalidClaimException:								失效的payload异常
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;
    }
}
6.4.6.2 加配置
jwt:
  scret: abced
  head: Authencation
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;
    }
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);

}
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)
})
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");
      }
    },
   }

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")
});

七、附录: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类的位置添加过滤器

你可能感兴趣的:(#,springboot项目,spring,java)