SpringSecurity与JWT整合(含案例代码)

参考文章:
https://www.bilibili.com/video/BV15a411A7kP?p=1
https://www.bilibili.com/video/BV1d7411y7Uu


1. 简介

   SpringSecurity在B站上讲的真的很糟糕,特别是与JWT的整合,看完视频后,依照老师的思路,自己整合了一下,现在讲自己的成果与大家分享,无需使用SpringCloud,只用了SpringBoot即可!在这里也推荐一下第二个b站的视频,他讲的SpringSecurity会稍微好一点,对代码也有会说明。


2. 类的说明

   在开始之间先说明一下需要用到几个SpringSecurity的类


2.1 UsernamePasswordAuthenticationFilter

    拦截登录请求并获取用户输入的账号和密码,然后把账号密码封装到 UsernamePasswordAuthenticationToken(未受信任的认证凭据)中,然后将 “认证凭据" 交给 我们配置AuthenticationManager去作认证

过程如下:
SpringSecurity与JWT整合(含案例代码)_第1张图片

理解了上述流程后,我们可以做以下事情:

  • 定制我们的登录请求URI和请求方式。
  • 登录请求参数的格式定制化。
  • 拦截登录。
  • 登录成功后调用的方法。
  • 登录失败后调用的方法。

  • 详见:https://felord.cn/usernamePasswordAuthenticationFilter.html


    2.2 AuthenticationManager

        认证管理器。WebSecurityConfigurerAdapter(配置SpringSecurity的类)中的void configure(AuthenticationManagerBuilder auth)是配置AuthenticationManager 的地方,里面只有一个Authentication authenticate(Authentication authentication)方法,他的作用是对 未受信任的认证凭据 做认证,认证成功则返回 授信状态的认证凭据 ,否则将抛出认证异常AuthenticationException


    详见:https://www.jianshu.com/p/56e53d4ec1a9


    2.3 Authentication

        认证对象,用于保存用户的信息。认证凭据 UsernamePasswordAuthenticationToken实现了该接口。属性如下:

    Boolean authenticated:确定当前用户是否受信,为true时受信,为false时不受信。上述的返回 授信状态的认证凭据,就是将该值设置为true
    Object principal:主体。在未受信的情况是,这是用户名;在已受信的情况下,这是UserDetails的实现类。
    Object credentials:在未受信的情况是,这是密码;在已受信的情况下,这是null,所以这里也可以设置JWT
    Collection authorities:主体的权限集合。由AuthenticationManager设置的,所以刚登录的时候,这里没有值;登录成功后主体中的权限会设置在这里。
    Object details:存储关于身份验证请求的其他详细信息。这些可能是IP地址、证书编号等。未使用返回null


    详见:https://felord.cn/securityContext.html

    2.4 SecurityContextHolder

        这是一个工具类,用于设置、获取、清理SecurityContext,这个SecurityContext是存储Authentication的容器,也就是说可以将受信用户存放到SecurityContext中,允许该请求的后续访问。


    详见:https://felord.cn/spring-security-securitycontext.html


    2.5 BasicAuthenticationFilter

        负责处理 HTTP 头中显示的基本身份验证凭据。
        重写void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) 即可从 HTTP 头获取JWT


    详见:https://felord.cn/spring-security-filters.html#3-24-BasicAuthenticationFilter


    3. 思路

    登录:
    SpringSecurity与JWT整合(含案例代码)_第2张图片


    退出登录:
    SpringSecurity与JWT整合(含案例代码)_第3张图片


    JWT认证:
    SpringSecurity与JWT整合(含案例代码)_第4张图片


    4. 核心代码说明

    SecurityConfig.class 配置类

    @Configuration
    public class TokenWebSecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Autowired
        private TokenManager tokenManager;
    
        @Autowired
        private RedisTemplate redisTemplate;
    
        @Autowired
        private UserDetailsService userDetailsService;
    
        @Bean
        PasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.csrf().disable()
                    .exceptionHandling()
                    .authenticationEntryPoint(new UnauthEnrtyPoint())       //无权限访问时,做出的处理
                    .and().authorizeRequests()
                    .antMatchers("/test/guest").permitAll()			//所有请求都可以访问
                    .antMatchers("/test/user").hasRole("user")		//只有角色为 user 的可以访问
                    .antMatchers("/test/admin").hasRole("admin")	//只有角色为 admin 的可以访问
                    .anyRequest().authenticated()
                    .and().logout()
                    .logoutUrl("/test/logout")     //设置退出路径
                    .addLogoutHandler(new TokenLogoutHandler(tokenManager,redisTemplate))   //退出时所做的逻辑
                    .and().addFilter(new TokenLoginFilter(tokenManager,redisTemplate,authenticationManager()))	//登录时的过滤器
                    .addFilter(new TokenAuthFilter(tokenManager,redisTemplate,authenticationManager()))		//验证JWT的过滤器
                    .httpBasic();
        }
    
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(userDetailsService)
                    .passwordEncoder(passwordEncoder());
        }
    }
    

    TokenLoginFilter.class 登录认证

    public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {
    
        private TokenManager tokenManager;
    
        private RedisTemplate redisTemplate;
    
        private AuthenticationManager authenticationManager;
    
        public TokenLoginFilter(TokenManager tokenManager, RedisTemplate redisTemplate, AuthenticationManager authenticationManager) {
            this.tokenManager = tokenManager;
            this.redisTemplate = redisTemplate;
            this.authenticationManager = authenticationManager;
            //定制我们的登录请求URI和请求方式。
            this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/test/login", "POST"));
        }
    
        /**
         * 拦截登录。获取表单的用户名与密码
         */
        @Override
        public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
            try {
                //使用 请求体 传递登录参数,更加安全
                LoginData user = new ObjectMapper().readValue(request.getInputStream(), LoginData.class);
                return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword()));
            } catch (IOException e) {
                e.printStackTrace();
                throw new RuntimeException();
            }
        }
    
        /**
         * 登录成功后调用的方法
         * 返回token
         */
        @Override
        protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
            SecurityUser user = (SecurityUser) authResult.getPrincipal();
    
            //根据用户名生成token
            String token = tokenManager.createToken(user.getUsername());
    
            response.setHeader("token", token);
    
            //将 用户名:角色 放入redis中
            redisTemplate.opsForValue().set(user.getUsername(), user.getRole().getRole_name());
        }
    
        /**
         * 登录失败后调用的方法
         */
        @Override
        protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
            response.getWriter().print("login fail");
        }
    }
    

    TokenAuthFilter.class 授权过滤,校验token的有效性

    public class TokenAuthFilter extends BasicAuthenticationFilter {
    
        private TokenManager tokenManager;
    
        private RedisTemplate redisTemplate;
    
        public TokenAuthFilter(TokenManager tokenManager, RedisTemplate redisTemplate, AuthenticationManager authenticationManager) {
            super(authenticationManager);
            this.tokenManager = tokenManager;
            this.redisTemplate = redisTemplate;
        }
    
        /**
         * 对HTTP请求头做处理
         */
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
            //授权
            UsernamePasswordAuthenticationToken authRequest = getAuthentication(request);
    
            //授权失败
            if (authRequest == null) {
                chain.doFilter(request, response);
                return;
            }
    
            //如果有授权,放到权限上下文(容器)中
            SecurityContextHolder.getContext().setAuthentication(authRequest);
            chain.doFilter(request, response);
        }
    
        /**
         * 认证token是否合法,若合法,返回认证,否则返回null
         */
        private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
            //获取token
            String token = request.getHeader("token");
            if (!StringUtils.isEmpty(token)) {
    
                //从token中获取username
                String username = tokenManager.getUserInfoFromToken(token);
    
                //redis中,根据username获取角色
                String role_name = (String) redisTemplate.opsForValue().get(username);
    
                Collection<GrantedAuthority> authorities = new ArrayList<>();
                List<GrantedAuthority> list = AuthorityUtils.commaSeparatedStringToAuthorityList(role_name);
                authorities.addAll(list);
    
                if (!StringUtils.isEmpty(username)) {
                    return new UsernamePasswordAuthenticationToken(username, token, authorities);
                }
            }
            return null;
        }
    }
    

    5. 源码

    https://github.com/Xavier-777/SpringSecurity-JWT

你可能感兴趣的:(jwt,java,spring,boot)