首页 专题 文章 代码 归档

SpringBoot Security整合jwt,作前后端分离项目

1. 前言

只能说,Spring Security确实是一个上手难度大,但是理解后,也非常好用的一个spring全家桶之一;

为了兼容、为了普遍(比如表单登录、第三方登录都可以使用security实现),只能说内部要复杂一点,接口要多一点,毕竟鱼和熊掌不可兼得;

2. 前提

学习security一定要理解其工作流程、原理以及其中各种类的作用;

这篇文章虽说我简单讲讲jwt,但是其实和form登录也差不多(form表单登录更简单一些);


先要明白:

spring security就是一个过滤器链,其中的每一个过滤器都有自己特定的功能;

其余的,不好写,先贴代码吧!

3. 代码

这里的用户没有从数据库中获取,而是在代码中固定写了一个用户;

3.1. 配置文件代码

1、配置文件com.misiai.jwt.config.SecurityConfig

package com.misiai.jwt.config;

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private JwtUtils jwtUtils;
    @Autowired
    private UserDetailsService userDetailsService;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = http.authorizeRequests();
        //让Spring security放行所有preflight request
        registry.requestMatchers(CorsUtils::isPreFlightRequest).permitAll();

        http.csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                .antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
                .antMatchers("/auth/**").permitAll()
                .anyRequest().authenticated()
                .and()
                .addFilterBefore(new JwtLoginFilter(authenticationManager(), jwtUtils),
                        UsernamePasswordAuthenticationFilter.class)
                .addFilterBefore(new JwtAuthenticationTokenFilter(userDetailsService, jwtUtils),
                        UsernamePasswordAuthenticationFilter.class)
                // .addFilter(new JwtLoginFilter(authenticationManager()))
                .headers().cacheControl();


        // http.addFilter(jwtLoginFilter);
        // http.addFilterBefore(jwtLoginFilter, UsernamePasswordAuthenticationFilter.class);
        // http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);


    }

    @Bean
    public CorsFilter corsFilter() {
        final UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource();
        final CorsConfiguration cors = new CorsConfiguration();
        cors.setAllowCredentials(true);
        cors.addAllowedOrigin("*");
        cors.addAllowedHeader("*");
        cors.addAllowedMethod("*");
        urlBasedCorsConfigurationSource.registerCorsConfiguration("/**", cors);
        return new CorsFilter(urlBasedCorsConfigurationSource);
    }

    @Bean(name = BeanIds.AUTHENTICATION_MANAGER)
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

说明:

这里主要重写了configure(HttpSecurity http)方法,这个方法中,主要:

1)禁用了csrf

2)禁用session,jwt应用不需要csrf和session

3)放行路径(/auth/**),主要用于登录等不需要验证的路径;

4)在UsernamePasswordAuthenticationFilter.class前面添加了两个自定义的过滤器(JwtLoginFilter,JwtAuthenticationTokenFilter)

3.2. 登录过滤器代码

登录过滤器:com.misiai.jwt.filter.JwtLoginFilter


@Slf4j
public class JwtLoginFilter extends UsernamePasswordAuthenticationFilter {

    private final JwtUtils jwtUtils;
    private final AuthenticationManager authenticationManager;

    public JwtLoginFilter(AuthenticationManager authenticationManager, JwtUtils jwtUtils) {
        this.authenticationManager = authenticationManager;
        this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/auth/login", "POST"));
        this.jwtUtils = jwtUtils;
    }


    @Override
    public Authentication attemptAuthentication(HttpServletRequest request,
                                                HttpServletResponse response) throws AuthenticationException {

        log.info("Authentication-->>attemptAuthentication");
        // 从输入流中获取到登录的信息
        try {
            UserEntity loginUser = new ObjectMapper().readValue(request.getInputStream(), UserEntity.class);
            return this.authenticationManager.authenticate(
                    new UsernamePasswordAuthenticationToken(loginUser.getUsername(), loginUser.getPassword())
            );
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }

    // 成功验证后调用的方法
    // 如果验证成功,就生成token并返回
    @Override
    protected void successfulAuthentication(HttpServletRequest request,
                                            HttpServletResponse response,
                                            FilterChain chain,
                                            Authentication authResult) throws IOException, ServletException {

        UserEntity user = (UserEntity) authResult.getPrincipal();
        System.out.println("user:" + user.toString());

        String role = "";
        Collection<? extends GrantedAuthority> authorities = user.getAuthorities();
        for (GrantedAuthority authority : authorities) {
            role = authority.getAuthority();
        }

        String token = jwtUtils.generateToken(user);
        //String token = JwtTokenUtils.createToken(user.getUsername(), false);
        // 返回创建成功的token
        // 但是这里创建的token只是单纯的token
        // 按照jwt的规定,最后请求的时候应该是 `Bearer token`
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json; charset=utf-8");
        String tokenStr = jwtUtils.getPrefix() + token;
        AppResult<String> appResult = AppResultBuilder.success(tokenStr, ResultCode.USER_LOGIN_SUCCESS);
        String s = new ObjectMapper().writeValueAsString(appResult);
        PrintWriter writer = response.getWriter();
        writer.print(s);//输出
        writer.close();
    }

    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {

        AppResult<String> appResult = AppResultBuilder.fail(ResultCode.USER_LOGIN_FAIL);
        response.getWriter().write(new ObjectMapper().writeValueAsString(appResult));
    }
}

说明:

0、其实这里JwtLoginFilter包括下面讲的JwtAuthenticationTokenFilter都是因为jwt应用,如果仅是form登录,那么完全不需要这两个类;

1、该类继承UsernamePasswordAuthenticationFilter,如果重写了登录、登录成功、登录失败逻辑;

2、这里面一共三个方法+一个构造方法

  • 构造方法主要接收了两个变量(如果使用注入,会报错),以及设置了处理[登录]的地址(“/auth/login”)
  • attemptAuthentication,也即尝试登录方法,老外喜欢使用attempt作为登录方法名,也能理解,“尝试登录”嘛
    • 在此方法中,我们主要从request中获取用户提交的username和password,并且构造一个UsernamePasswordAuthenticationToken
    • UsernamePasswordAuthenticationToken仅仅是包括usernamepassword的一个类而已,真正验证密码是否正确的逻辑实在this.authenticationManager.authenticate这个方法内,
  • successfulAuthentication方法就是登录成功后的逻辑,这里的示例代码的处理就是返回一个json,包括token的json,前后端分离嘛
  • unsuccessfulAuthentication就是登录失败的逻辑,也是返回json

3.3. 用户携带token认证过滤器

前面代码是用户登录的逻辑,登录是登录了,也返回了token,但是前后端分离项目,是不使用session,那么spring如何知道用户是否登录了?

所以,我们需要在过滤器中来判断用户是否携带token(也即是否登录)

如果携带了token并且token有效的话,那么久封装用户的一些信息到security的全局上下文中(SecurityContextHolder

这样,security后面的过滤器就知道用户是否登录了,并且有哪些权限!


@Slf4j
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {


    /*通过构造参数注入*/
    private final UserDetailsService userDetailsService;

    private final JwtUtils jwtutils;

    public JwtAuthenticationTokenFilter(UserDetailsService userDetailsService, JwtUtils jwtutils) {
        this.userDetailsService = userDetailsService;
        this.jwtutils = jwtutils;
    }


    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
        String authHeader = request.getHeader(jwtutils.getHeader());
        if (StringUtils.isNotEmpty(authHeader)) {

            String username = jwtutils.getUsernameFromToken(authHeader);
            log.info("加入凭证:{}", username);
            if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                // 判断如果username不为空,且上下文中没有数据,那么就尝试验证,
                UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
                log.info("Details:{}", userDetails.toString());


                if (jwtutils.validateToken(authHeader, userDetails)) {
                    // 且验证成功后,在上下文中加入凭证
                    UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                    authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                }
            }
        }
        chain.doFilter(request, response);
    }
}

总结JwtAuthenticationTokenFilter和JwtLoginFilter:

1、一句话:在默认的表单登录,是不需要这两个方法的,因为对于表单登录,security是由默认实现的;

而我们想要使用jwt应用,那么就需要重写登录逻辑,为什么要重写?

1)登录信息我们也许是使用json传递的,而不是application/x-www-form-urlencode

2)我们需要返回的是json,而不是跳转或是其他

2、如果是传统的前后端项目,那么就是使用session,security会自动从session中提取用户信息,但是前后端分离项目我们不会使用session,那么登录后,security如何知道用户已经登录了?就需要我们自定义一个过滤器,判断如果携带有token,那么就把用户信息保存到全局上下文中


当然,以上两个过滤器仅是一个大致流程,其中还涉及到了很多细节和坑;

3.4. UserDetails

细节之一:UserDetails

UserDetails是一个接口,主要是有几个方法,来告诉security用户的一些状态,其源码:

public interface UserDetails extends Serializable {

    Collection<? extends GrantedAuthority> getAuthorities();

    String getPassword();

    String getUsername();

    boolean isAccountNonExpired();

    boolean isAccountNonLocked();

    boolean isCredentialsNonExpired();

    boolean isEnabled();
}

比如用户是否正常,是否被禁止(如禁止登陆)等等信息。

一般情况下,我们就把实体类实现该接口

3.5. UserDetailsService

细节二:UserDetailsService

该接口只有一个方法:

UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;

通过用户名获取用户的信息,返回的是UserDetails

这里实际情况下,就是从数据库中获取用户信息

而一般情况下,我们也是在原本的service层实现该接口,当然,另写一个类实现也可

4. 附录

4.1. jwt工具类

package com.misiai.jwt.util;

@Data
@ConfigurationProperties(prefix = "jwt")
@Component
@Slf4j
public class JwtUtils implements Serializable {

    private String secret;

    private Long expiration;

    private String header;

    private String prefix;


    /**
     * 处理token,把"Bearer "去掉
     * @param originToken 待处理的token
     * @return 处理后的token
     */
    private String processToken(String originToken) {
        try {
            String[] split = originToken.split("\\s");
            if (split.length >= 2) {
                return split[1];
            }
        } catch (Exception ignored) {
        }
        return originToken;
    }

    /**
     * 从数据声明生成令牌
     *
     * @param claims 数据声明
     * @return 令牌
     */
    private String generateToken(Map<String, Object> claims) {
        Date expirationDate = new Date(System.currentTimeMillis() + expiration);
        return Jwts.builder().setClaims(claims).setExpiration(expirationDate).signWith(SignatureAlgorithm.HS512, secret).compact();
    }

    /**
     * 从令牌中获取数据声明
     *
     * @param token 令牌
     * @return 数据声明
     */
    private Claims getClaimsFromToken(String token) {
        String newToken = processToken(token);
        Claims claims;
        try {
            claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(newToken).getBody();
        } catch (Exception e) {
            claims = null;
        }
        return claims;
    }

    /**
     * 生成令牌
     *
     * @param userDetails 用户
     * @return 令牌
     */
    public String generateToken(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>(2);
        claims.put("sub", userDetails.getUsername());
        claims.put("created", new Date());
        return generateToken(claims);
    }

    /**
     * 从令牌中获取用户名
     *
     * @param token 令牌
     * @return 用户名
     */
    public String getUsernameFromToken(String token) {
        String newToken = processToken(token);
        String username;
        try {
            Claims claims = getClaimsFromToken(newToken);
            username = claims.getSubject();
        } catch (Exception e) {
            username = null;
        }
        return username;
    }

    /**
     * 判断令牌是否过期
     *
     * @param token 令牌
     * @return 是否过期
     */
    public Boolean isTokenExpired(String token) {
        String newToken = processToken(token);
        try {
            Claims claims = getClaimsFromToken(newToken);
            Date expiration = claims.getExpiration();
            return expiration.before(new Date());
        } catch (Exception e) {
            return false;
        }
    }

    /**
     * 刷新令牌
     *
     * @param token 原令牌
     * @return 新令牌
     */
    public String refreshToken(String token) {
        String newToken = processToken(token);
        String refreshedToken;
        try {
            Claims claims = getClaimsFromToken(newToken);
            claims.put("created", new Date());
            refreshedToken = generateToken(claims);
        } catch (Exception e) {
            refreshedToken = null;
        }
        return refreshedToken;
    }

    /**
     * 验证令牌
     *
     * @param token       令牌
     * @param userDetails 用户
     * @return 是否有效
     */
    public Boolean validateToken(String token, UserDetails userDetails) {
        String newToken = processToken(token);
        User user = (User) userDetails;
        String username = getUsernameFromToken(newToken);
        return (username.equals(user.getUsername()) && !isTokenExpired(newToken));
    }
}

4.2. 配置文件

上面的工具类需要在配置文件新建一些值:

jwt:
  secret: 2fafwefaffdafd
  expiration: 900000
  header: Authentication
  prefix: "Bearer "
此文阅读完毕,您可以:分享
二维码图片 扫描关注我们哟