Spring Security整合JWT

300 阅读3分钟

JWT

JWT(JSON Web Token),这种方式服务器端就不需要保存Session数据了,只用在客户端保存服务端返回给客户的Token就可以了,扩展性得到提升。JWT 本质上就一段签名的JSON格式的数据。由于它是带有签名的,因此接收者便可以验证它的真实性。 详情请看:认证授权这一篇文章

引入依赖

jjwt依赖封装了JWT一些常用的工具,用于生成JWT和解析JWT

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

创建用户对象实现UserDetails接口

UserDetails是Spring Security提供的一个接口,可以定义一些登录用户的属性

@Setter
@Getter
@ToString
public class User implements UserDetails {

    private String username;

    private String password;

    private List<GrantedAuthority> authorities;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return username;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

定义测试接口

引入Spring Security依赖后,默认所有接口是封闭的,必须登录后才能访问,这时候就需要一些过滤器做一些校验规则。

@RestController
public class HelloController {
    @GetMapping("/hello")
    public String hello() {
        return "hello, jwt!";
    }

    @GetMapping("/admin")
    public String admin() {
        return "hello, admin!";
    }
}

用户登录的过滤器

这个过滤器主要用于用户登录成功后,生成JWT返回前端,失败后显示错误提示。只会拦截defaultFilterProcessesUrl

public class JwtLoginFilter extends AbstractAuthenticationProcessingFilter {

    /**
     *
     * @param defaultFilterProcessesUrl 默认拦截的URL
     * @param authenticationManager
     */
    protected JwtLoginFilter(String defaultFilterProcessesUrl, AuthenticationManager authenticationManager) {
        super(defaultFilterProcessesUrl);
        setAuthenticationManager(authenticationManager);
    }

    /**
     * 验证账号密码是否正确
     * 
     * @param httpServletRequest
     * @param httpServletResponse
     * @return
     * @throws AuthenticationException
     * @throws IOException
     * @throws ServletException
     */
    @Override
    public Authentication attemptAuthentication(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws AuthenticationException, IOException, ServletException {
        User user = new ObjectMapper().readValue(httpServletRequest.getInputStream(), User.class);
        Authentication authenticate = getAuthenticationManager().authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword()));
        return authenticate;
    }

    /**
     * 验证成功
     * 
     * @param request
     * @param response
     * @param chain
     * @param authResult
     * @throws IOException
     * @throws ServletException
     */
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        Collection<? extends GrantedAuthority> authorities = authResult.getAuthorities();
        StringBuffer stringBuffer = new StringBuffer();
        for (GrantedAuthority authority : authorities) {
            stringBuffer.append(authority.getAuthority()).append(",");
        }
        // 生成JWT,过期时间为1小时
        String jwt = Jwts.builder().claim("authorities", stringBuffer.toString())
                .setSubject(authResult.getName())
                .setExpiration(new Date(System.currentTimeMillis() + 60 * 60 * 1000))
                .signWith(SignatureAlgorithm.HS512, "tong@123")
                .compact();
        response.setContentType("application/json;charset=utf-8");
        PrintWriter writer = response.getWriter();
        writer.write(new ObjectMapper().writeValueAsString(jwt));
        writer.flush();
        writer.close();
    }

    /**
     * 验证失败
     * 
     * @param request
     * @param response
     * @param failed
     * @throws IOException
     * @throws ServletException
     */
    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
        response.setContentType("application/json;charset=utf-8");
        PrintWriter writer = response.getWriter();
        writer.write("登录失败");
        writer.flush();
        writer.close();
    }
}

校验token的过滤器

用户访问接口的时候,校验接口是否携带token,以及token是否正确,如果正确就放行,如果不正确返回错误

public class JwtFilter extends GenericFilterBean {

    /**
     * 校验token是否正确
     *
     * @param servletRequest
     * @param servletResponse
     * @param filterChain
     * @throws IOException
     * @throws ServletException
     */
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        String token = request.getHeader("authorization");
        if (StringUtils.isEmpty(token)) {
            response.setContentType("application/json;charset=utf-8");
            PrintWriter writer = response.getWriter();
            writer.write("token不能空");
            writer.flush();
            writer.close();
        }
        try {
            Claims claims = Jwts.parser().setSigningKey("tong@123").parseClaimsJws(token.replace("Bearer", "")).getBody();
            String username = claims.getSubject();
            List<GrantedAuthority> authorization = AuthorityUtils.commaSeparatedStringToAuthorityList((String) claims.get("authorities"));
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, null, authorization);
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        } catch (ExpiredJwtException e) {
            response.setContentType("application/json;charset=utf-8");
            PrintWriter writer = response.getWriter();
            writer.write("token失效");
            writer.flush();
            writer.close();
        }
        filterChain.doFilter(servletRequest, servletResponse);
    }
}

Spring Security配置

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * PasswordEncoder密码加密, 这里使用的是不加密,推荐使用BCryptPasswordEncoder加密
     *
     * @return
     */
    @Bean
    PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }

    /**
     * 在内存里生生成一些用户
     *
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication().withUser("admin")
                .password("123")
                .roles("admin")
                .and()
                .withUser("tong")
                .password("456")
                .roles("user");
    }

    /**
     * /hello 接口必须要具备 user 角色才能访问,
     * /admin 接口必须要具备 admin 角色才能访问,
     * POST 请求并且是 /login 接口则可以直接通过,
     * 其他接口必须认证后才能访问
     *
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/hello").hasRole("user")
                .antMatchers("/admin").hasRole("admin")
                .antMatchers(HttpMethod.POST, "/login").permitAll()
                .anyRequest().authenticated()
                .and()
                .addFilterBefore(new JwtFilter(), UsernamePasswordAuthenticationFilter.class)
                .addFilterBefore(new JwtLoginFilter("/login", authenticationManager()), JwtFilter.class)
                .csrf().disable();
    }
}

访问

  • 不登陆直接访问hello接口,直接报错token不能为空

  • 输入错误密码,直接报登录失败

  • 输入正确的用户密码,登录成功并返回token

  • 访问hello接口,返回成功