当前位置:   article > 正文

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

spring security整合rbac

三、原理解析

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.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
</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 引入依赖
<!--引入jwt-->
<dependency>
  <groupId>com.auth0</groupId>
  <artifactId>java-jwt</artifactId>
  <version>3.4.0</version>
</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类的位置添加过滤器
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Cpp五条/article/detail/436974
推荐阅读
相关标签
  

闽ICP备14008679号