SpringSecurity - 整合JWT使用 Token 认证授权

Posted 小毕超

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了SpringSecurity - 整合JWT使用 Token 认证授权相关的知识,希望对你有一定的参考价值。

一、SpringSecurity

前面讲解了SpringSecurity的动态认证和动态权限角色,我们都知道在现在大多都是微服务前后端分离的模式开发,前面讲的还是基于Session的,本篇我们整合JWT实现使用Token认证授权。

上篇文章地址:https://blog.csdn.net/qq_43692950/article/details/122394611

在开始前需要了解JWT,如果不了解,可以先看下下面这篇我的博客:

https://blog.csdn.net/qq_43692950/article/details/107443397

二、SpringSecurity 整合JWT使用 Token 认证授权

本篇文章还是接着上篇文章进行讲解,数据库还是使用上两篇文章中创建的数据库:

由于我们要使用JWT生成Token和存储一些信息,所以先引入JWT的依赖:

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.6.0</version>
</dependency>

编写JwtTool工具:

@Data
@Component
public class JwtTool 
    private String key = "com.bxc";
    private long overtime = 1000 * 60 * 60;

    public String CreateToken(String userid, String username, List<String> roles) 
        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);
        JwtBuilder builder = Jwts.builder()
                .setId(userid)
                .setSubject(username)
                .setIssuedAt(now)
                .signWith(SignatureAlgorithm.HS256, key)
                .claim("roles", roles);
        if (overtime > 0) 
            builder.setExpiration(new Date(nowMillis + overtime));
        
        return builder.compact();
    

    public boolean VerityToken(String token) 
        try 
            Claims claims = Jwts.parser()
                    .setSigningKey(key)
                    .parseClaimsJws(token)
                    .getBody();
            if (claims != null) 
                return true;
            
         catch (Exception e) 
            e.printStackTrace();
        
        return false;
    

    public String getUserid(String token) 
        try 
            Claims claims = Jwts.parser()
                    .setSigningKey(key)
                    .parseClaimsJws(token)
                    .getBody();
            if (claims != null) 
                return claims.getId();
            
         catch (Exception e) 
            e.printStackTrace();
        
        return null;
    

    public String getUserName(String token) 
        try 
            Claims claims = Jwts.parser()
                    .setSigningKey(key)
                    .parseClaimsJws(token)
                    .getBody();
            if (claims != null) 
                return claims.getSubject();
            
         catch (Exception e) 
            e.printStackTrace();
        
        return null;
    

    public List<String> getUserRoles(String token) 
        try 
            Claims claims = Jwts.parser()
                    .setSigningKey(key)
                    .parseClaimsJws(token)
                    .getBody();
            if (claims != null) 
                return (List<String>) claims.get("roles");
            
         catch (Exception e) 
            e.printStackTrace();
        
        return null;
    

    public String getClaims(String token, String param) 
        try 
            Claims claims = Jwts.parser()
                    .setSigningKey(key)
                    .parseClaimsJws(token)
                    .getBody();
            if (claims != null) 
                return claims.get(param).toString();
            
         catch (Exception e) 
            e.printStackTrace();
        
        return null;
    

使用CreateToken就可以生成下面这种字符串:

eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4Iiwic3ViIjoiYWRtaW4iLCJpYXQiOjE2NDE3Mjg5NzgsInJvbGVzIjpbIlJPTEVfYWRtaW4iXSwiZXhwIjoxNjQxNzMyNTc4fQ.qI_pXiwm2IzZNRdyKvTRuSj0JxiPHepPOXg_u6AAE88

我们就使用类似上面这串做我们的Token。

既然使用Token了,肯定大多都是采用前后端分离架构,一般都是采用JSON进行交互的,但细心的会发现,前面的登录都是一个form表单的形式,所以第一步我们先把登录换成JSON的形式。

下面就需要重写自己的登录过滤器,需要实现UsernamePasswordAuthenticationFilter接口,其中attemptAuthentication(HttpServletRequest request, HttpServletResponse response) 表示获取用户用户名密码的入口,我们可以在这里自定义接受用户名密码,比如以json形式接受,然后再传递给Security,然后Security下面就会去调用UserDetailsService做用户名和密码的正确性验证,如果用户名密码正确那就是登录成功,就会触发该实现下的successfulAuthentication方法,否则就是unsuccessfulAuthentication方法,我们可以在相应的方法中编写相应的提示返回给客户端:

public class LoginAuthenticationFilter extends UsernamePasswordAuthenticationFilter 

    private JwtTool jwtTool;

    public LoginAuthenticationFilter(AuthenticationManager authenticationManager, JwtTool jwtTool) 
        this.setAuthenticationManager(authenticationManager);
        this.jwtTool = jwtTool;
//        this.setPostOnly(false);
//        this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/login","POST"));
    


    @SneakyThrows
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException 
        response.setContentType("text/json;charset=utf-8");
        if (request.getContentType().equals(MediaType.APPLICATION_JSON_UTF8_VALUE) || request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE)) 
            BufferedReader br = null;
            try 
                br = new BufferedReader(new InputStreamReader(request.getInputStream(), "utf-8"));
                String line = null;
                StringBuilder sb = new StringBuilder();
                while ((line = br.readLine()) != null) 
                    sb.append(line);
                
                JSONObject json = JSONObject.parseObject(sb.toString());
                System.out.println(json.toString());

                String username = json.getString("username");
                String password = json.getString("password");

                UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
                this.setDetails(request, authRequest);
                return this.getAuthenticationManager().authenticate(authRequest);
             catch (IOException e) 
                e.printStackTrace();
                response.getWriter().write(JSON.toJSONString(ResponseTemplate.builder().code(400).message("参数错误!").build()));
            
         else 
            return super.attemptAuthentication(request, response);
        
        response.getWriter().write(JSON.toJSONString(ResponseTemplate.builder().code(400).message("参数错误!").build()));
        return null;
    

    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response,
                                            FilterChain chain, Authentication authentication) throws IOException, ServletException 
        UserEntity user = (UserEntity) authentication.getPrincipal();
        String username = user.getUsername();
        List<GrantedAuthority> authorities = (List<GrantedAuthority>) user.getAuthorities();
        List<String> roles = authorities.stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList());
        String token = jwtTool.CreateToken(String.valueOf(user.getId()), username, roles);
        response.setContentType("text/json;charset=utf-8");
        Map<String, Object> map = new HashMap<>();
        map.put("username", username);
        map.put("role", roles);
        map.put("token", token);
        response.getWriter().write(JSON.toJSONString(ResponseTemplate.builder().data(map).build()));

    

    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException 
        response.setContentType("text/json;charset=utf-8");

        if (e instanceof UsernameNotFoundException || e instanceof BadCredentialsException) 
            response.getWriter().write(JSON.toJSONString(ResponseTemplate.builder().code(400).message("用户名或密码错误!").build()));
         else if (e instanceof DisabledException) 
            response.getWriter().write(JSON.toJSONString(ResponseTemplate.builder().code(400).message("账户被禁用,请联系管理员!").build()));
         else 
            response.getWriter().write(JSON.toJSONString(ResponseTemplate.builder().code(400).message("用户名或密码错误!").build()));

        
    


登录成功后,我们使用Jwt生成了一串Token并返还给用户,以后的所有请求都需要携带该Token,但Security 默认的是从Session中获取用户信息,显然也不符合我们的要求,所以下面我们要重写自己的Token过滤器。

需要实现BasicAuthenticationFilter接口,我们只需在doFilterInternal中做自己的逻辑即可,如果全部OK就放行该过滤器即可:

@Slf4j
public class JWTAuthenticationFilter extends BasicAuthenticationFilter 

    private JwtTool jwtTool;

    public JWTAuthenticationFilter(AuthenticationManager authenticationManager, JwtTool jwtTool) 
        super(authenticationManager);
        this.jwtTool = jwtTool;
    

    public JWTAuthenticationFilter(AuthenticationManager authenticationManager, AuthenticationEntryPoint authenticationEntryPoint) 
        super(authenticationManager, authenticationEntryPoint);
    

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException 
        response.setContentType("text/json;charset=utf-8");
        String token = request.getHeader("token");

        if (StringUtils.isEmpty(token))
            response.getWriter().write(JSON.toJSONString(ResponseTemplate.builder().code(401).message("登录失效!").build()));
            return;
        

        boolean isold = jwtTool.VerityToken(token);

        if (!isold) 
            response.getWriter().write(JSON.toJSONString(ResponseTemplate.builder().code(401).message("登录失效!").build()));
            return;
        

        String username = jwtTool.getUserName(token);

        if (StringUtils.isEmpty(username)) 
            response.getWriter().write(JSON.toJSONString(ResponseTemplate.builder().code(401).message("登录失效!").build()));
            return;
        

        List<String> roles = jwtTool.getUserRoles(token);
        if (roles.isEmpty()) 
            response.getWriter().write(JSON.toJSONString(ResponseTemplate.builder().code(403).message("权限不足!").build()));
            return;
        
        List<GrantedAuthority> authorities = roles.stream().map(r -> new SimpleGrantedAuthority(r)).collect(Collectors.toList());
        UserEntity principal = new UserEntity();
        principal.setUsername(username);
        try 
            UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(principal, null, authorities);
            SecurityContextHolder.getContext().setAuthentication(authentication);
            String userid = jwtTool.getUserid(token);
            request.setAttribute("userid", userid);
            request.setAttribute("username", username);
//            request.setAttribute("role", role);
            chain.doFilter(request, response);
         catch (Exception e) 
            e.printStackTrace();
            response.getWriter().write(JSON.toJSONString(ResponseTemplate.builder().code(401).message("登录失效!").build()));
        
    

上面我们重写了接受用户名密码,和校验Token的过滤器,显然已经符合我们前后端分离架构,但是还有一个就是无权限的返回,在上两篇就可以看出,无权限是返回的403错误,显然也不符合,应该要修改为JSON的返回。

我们可以实现AccessDeniedHandler这个接口,来做无权限自定义的返回:

@Component
public class AuthenticationAccessDeniedHandler implements AccessDeniedHandler 

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException)
            throws IOException, ServletException 
        response.setContentType("text/json;charset=utf-8");
        response.getWriter().write(JSON.toJSONString(ResponseTemplate.builder().code(403).message("权限不足!").build()));
    


最后修改WebSecurityConfig,将上面的过滤器添加到Security 中,替换到默认的过滤器:

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter 

    @Autowired
    UserService userService;

    @Autowired
    CustomAccessDecisionManager customAccessDecisionManager;

    @Autowired
    CustomFilterInvocationSecurityMetadataSource customFilterInvocationSecurityMetadataSource;

    @Autowired
    AuthenticationAccessDeniedHandler authenticationAccessDeniedHandler;

    @Autowired
    JwtTool jwtTool;

    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception 
        auth.userDetailsService(userService).passwordEncoder(password());
    

    @Bean
    PasswordEncoder password() 
        return new BCryptPasswordEncoder();
    

    @Override
    protected void configure(HttpSecurity http) throws Exception 
        http.authorizeRequests()
                .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() 
                    @Override
                    public <O extends FilterSecurityInterceptor> O postProcess(O o) 
                        o.setSecurityMetadataSource(customFilterInvocationSecurityMetadataSource);
                        o.setAccessDecisionManager(customAccessDecisionManager);
                        return o;
                    
                )
                .antMatchers("/**").fullyAuthenticated()
                .and()
                .formLogin()
                .loginProcessingUrl("/login")
                .permitAll()
                .and()
                .exceptionHandling()
                .accessDeniedHandler(authenticationAccessDeniedHandler)
                .and()
                .addFilter(new JWTAuthenticationFilter(authenticationManager(), jwtTool))
                .addFilter(new LoginAuthenticationFilter(authenticationManager(),jwtTool))
                .csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    

    @Override
    public void configure(WebSecurity web) throws Exception 
        web.ignoring().antMatchers("/register/**");
    

    @Bean
    RoleHierarchy roleHierarchy() 
        RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
        String hierarchy = "ROLE_admin > ROLE_user > ROLE_common";
        roleHierarchy.setHierarchy(hierarchy);
        return roleHierarchy;
    

相比于上一次的修改,这里就是通过addFilter的方式添加我们的过滤器。

下面就可以启动项目,访问http://localhost:8080/admin/test测试接口:

直接就是返回登录失效了,下面我们使用PostMan登录:http://localhost:8080/login

可以看到这里我把权限也返回出来进行测试,表示该用户只能访问admin/**,下面我们使用Token访问http://localhost:8080/admin/test

如果访问common/**:

到这里就实现了使用Jwt Token的认证授权了。


喜欢的小伙伴可以关注我的个人微信公众号,获取更多学习资料!

以上是关于SpringSecurity - 整合JWT使用 Token 认证授权的主要内容,如果未能解决你的问题,请参考以下文章

从零玩转SpringSecurity+JWT整合前后端分离-从零玩转springsecurityjwt整合前后端分离

SpringSecurity整合JWT

SpringSecurity整合Jwt

SpringSecurity整合JWT-详细版

SpringBoot-SpringSecurity-Jwt的整合案例分析

SpringBoot-SpringSecurity-Jwt的整合案例分析