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的认证授权了。

在这里插入图片描述
喜欢的小伙伴可以关注我的个人微信公众号,获取更多学习资料!

评论 16
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小毕超

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值