美文网首页springboot2SpringBoot精选Spring Boot
SpringBoot 全家桶 | SpringSecurity

SpringBoot 全家桶 | SpringSecurity

作者: 码农StayUp | 来源:发表于2020-09-13 23:42 被阅读0次

    本文源码:Gitee·点这里

    前言

    本篇主要讲述 Spring Security 如何结合 JWT ,实现无状态下用户登录,使其满足前后端分离及应用集群化部署要求。

    什么是有状态

    有状态服务是服务端记录客户端会话信息,即Session信息。客户端每次请求都会携带Session信息,服务端以此来识别客户端身份。而 Session 保存在服务端内存中的,不支持集群化部署。

    当然 Spring 也给出了解决方案,即使用特殊方式将 Session 序列化存入到数据库中,以实现会话共享,满足集群化部署要求。详细案例参见《SpringBoot 全家桶 | SpringSession + Redis实现会话共享》

    什么是无状态

    无状态服务即服务端不保存任何客户端会话信息,而是由客户端每次请求必须携带自描述信息,服务端通过这些信息来识别客户端身份。JWT便是无状态的一种实现标准

    什么是JWT

    JSON Web Token (JWT)是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。

    JWT由三部分组成,这些部分由点(.)分隔,分别是:

    • Header 标头
    • Payload 有效载体
    • Signature 签名

    因此,JWT通常通常是这样子的:xxxxx.yyyyy.zzzzz

    更多详细内容参见官网:JSON Web Tokens

    JWT如何工作

    下图显示了如何获取JWT并将其用于访问API或资源:

    client-credentials-grant
    1. 用程序或客户端向授权服务器请求授权。
    2. 授权后,授权服务器会将访问令牌返回给应用程序。
    3. 该应用程序使用访问令牌来访问受保护的资源(例如API)。

    本案例使用框架

    完整代码 - 码云

    springboot-security-jwt

    Spring Security 集成 JWT

    pom文本引入io.jsonwebtoken.jjwt

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

    增加JWT工具类

    public class JwtUtil {
    
        public static final String TOKEN_HEADER = "Authorization";
        public static final String TOKEN_PREFIX = "Bearer";
        public static final long TTL = 2 * 60 * 60 * 1000;
        public static final int TTL_COOKIE = 2 * 60 * 60;
        private static final String SECRET_KEY = "https://www.jianshu.com/u/1b5928185b73";
        private static final String AUTHORITIES = "authorities";
    
        /**
         * 生成 token
         *
         * @param username
         * @return
         */
        public static String generateToken(String username) {
            return generateToken(username, new ArrayList<>());
        }
    
        /**
         * 生成 token
         *
         * @param username
         * @param authorities
         * @return
         */
        public static String generateToken(String username, List<String> authorities) {
            return Jwts.builder()
                    .setSubject(username) // 主题
                    .claim(AUTHORITIES, authorities)
                    .setIssuedAt(new Date()) // 发布时间
                    .setExpiration(new Date(System.currentTimeMillis() + TTL)) // 到期时间
                    .signWith(SignatureAlgorithm.HS256, SECRET_KEY) // 签名
                    .compact();
        }
    
        /**
         * 生成 token
         *
         * @param claims
         * @return
         */
        private static String generateToken(Claims claims) {
            return Jwts.builder()
                    .setClaims(claims)
                    .setExpiration(new Date(System.currentTimeMillis() + TTL)) // 到期时间
                    .signWith(SignatureAlgorithm.HS256, SECRET_KEY) // 签名
                    .compact();
        }
    
        /**
         * 解析 token
         *
         * @param token
         * @return
         */
        public static Claims parseToken(String token) {
            return Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody();
        }
    
        /**
         * 获取 username
         *
         * @param token
         * @return
         */
        public static String getUsername(String token) {
            return parseToken(token).getSubject();
        }
    
        /**
         * 获取 username
         *
         * @param claims
         * @return
         */
        public static String getUsername(Claims claims) {
            return claims.getSubject();
        }
    
        /**
         * 是否过期
         *
         * @param token
         * @return
         */
        public static boolean isExpiration(String token) {
            return parseToken(token).getExpiration().before(new Date());
        }
    
        /**
         * 是否过期
         *
         * @param claims
         * @return
         */
        public static boolean isExpiration(Claims claims) {
            return claims.getExpiration().before(new Date());
        }
    
        /**
         * 获取角色
         *
         * @param token
         * @return
         */
        public static List<String> getAuthorities(String token) {
            return parseToken(token).get(AUTHORITIES, List.class);
        }
    
        /**
         * 获取角色
         *
         * @param claims
         * @return
         */
        public static List<String> getAuthorities(Claims claims) {
            return claims.get(AUTHORITIES, List.class);
        }
    
        /**
         * 刷新 token
         *
         * @param token
         * @return
         */
        public static String refreshToken(String token) {
            return generateToken(parseToken(token));
        }
    
    }
    
    

    Security配置类增加JWT配置

    此处不详细介绍Security的配置,而是把重点放在集成JWT上。(Security请参阅 《SpringBoot 全家桶 | SpringSecurity实战》

    @EnableWebSecurity
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
        private final Logger log = LoggerFactory.getLogger(this.getClass());
    
        @Resource
        private UserDao userDao;
    
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests()
                    .antMatchers("/css/**", "/js/**", "/", "/index", "/loginPage").permitAll() // 无需认证
                    .anyRequest().authenticated() // 其他请求都需要认证
            ;
    
            http.addFilter(new JwtAuthorizationFilter(authenticationManager()));
    
            http.formLogin() // 开启登录,如果没有权限,就会跳转到登录页
                    .loginPage("/loginPage") // 自定义登录页,默认/login(get请求)
                    .loginProcessingUrl("/login") // 登录处理地址,默认/login(post请求)
                    .usernameParameter("inputEmail") // 自定义username属性名,默认username
                    .passwordParameter("inputPassword") // 自定义password属性名,默认password
                    .successHandler(loginSuccessHandler())
            ;
    
            http.rememberMe() // 开启记住我
                    .rememberMeParameter("rememberMe") // 自定义rememberMe属性名
            ;
    
            http.logout() // 开启注销
                    .logoutUrl("/logout") // 注销处理路径,默认/logout
                    .logoutSuccessUrl("/") // 注销成功后跳转路径
                    .deleteCookies(TOKEN_HEADER) // 删除cookie
            ;
    
            http.csrf().disable(); // 禁止csrf
    
            http.sessionManagement() // session管理
                    .sessionCreationPolicy(SessionCreationPolicy.STATELESS); // 无状态,即不创建Session,也不使用SecurityContext获取Session
        }
    
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(userDetailsService())
                    .passwordEncoder(passwordEncoder());
        }
    
        @Bean
        public UserDetailsService userDetailsService() {
            return username -> {
                xyz.zyl2020.securityjwt.entity.User user = userDao.findByUsernameOrEmail(username, username);
                if (user == null) {
                    throw new UsernameNotFoundException("账号或密码错误!");
                }
                String[] roleCodeArray = user.getRoles().stream().map(Role::getCode).toArray(String[]::new);
    
                return User.withUsername(user.getUsername())
                        .password(user.getPassword())
                        .authorities(roleCodeArray)
                        .build();
            };
        }
    
    
        @Bean
        public PasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }
    
        @Bean
        public AuthenticationSuccessHandler loginSuccessHandler() {
            return (request, response, authentication) -> {
                List<String> authorities = new ArrayList<>();
                if (!CollectionUtils.isEmpty(authentication.getAuthorities())) {
                    authorities.addAll(authentication.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList()));
                }
                String token = JwtUtil.generateToken(authentication.getName(), authorities);
                // 将token添加到header中
                response.setHeader(TOKEN_HEADER, JwtUtil.TOKEN_PREFIX + token);
                // 将token添加到cookie中
                Cookie cookie = new Cookie(TOKEN_HEADER, JwtUtil.TOKEN_PREFIX + token);
                cookie.setPath("/");
                cookie.setMaxAge(JwtUtil.TTL_COOKIE);
                response.addCookie(cookie);
                log.info("登录成功,username: {}, token: {}", authentication.getName(), token);
            };
        }
    }
    
    
    • http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)Session管理配置为无状态,这样Security便不会创建Session
    • loginSuccessHandler() 登录成功处理器,用于登录成功后,使用生成JWT工具类生成token,并将token添加到headercookie中(添加到cookie的目标是为了兼容未做前后端分离的应用)给客户端响应。
    • http.addFilter(new JwtAuthorizationFilter(authenticationManager())) 增加 JWT 授权过滤器,后面详细介绍
    • http.logout().deleteCookies(TOKEN_HEADER) 用户注销后删除token

    JWT 授权过滤器

    添加此过滤器的目的是客户端每次请求时,验证其令牌是否合法

    public class JwtAuthorizationFilter extends BasicAuthenticationFilter {
    
        public JwtAuthorizationFilter(AuthenticationManager authenticationManager) {
            super(authenticationManager);
        }
    
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
    
            String headerToken = "";
            // 从cookie中获取token
            Cookie[] cookies = request.getCookies();
            if (cookies != null) {
                for (Cookie cookie : cookies) {
                    if (JwtUtil.TOKEN_HEADER.equals(cookie.getName()) && StringUtils.isNotBlank(cookie.getValue())) {
                        headerToken = cookie.getValue();
                        break;
                    }
                }
            }
            // 从header中获取token
            if (StringUtils.isBlank(headerToken) && StringUtils.isNotBlank(request.getHeader(JwtUtil.TOKEN_HEADER))) {
                headerToken = request.getHeader(JwtUtil.TOKEN_HEADER);
            }
            // 从参数中获取token
            if (StringUtils.isBlank(headerToken) && StringUtils.isNotBlank(request.getParameter(JwtUtil.TOKEN_HEADER))) {
                headerToken = request.getParameter(JwtUtil.TOKEN_HEADER);
            }
    
            // 校验token头
            if (StringUtils.isBlank(headerToken) || !headerToken.startsWith(JwtUtil.TOKEN_PREFIX)) {
                chain.doFilter(request, response);
                return;
            }
            // 解析token
            String token = headerToken.substring(JwtUtil.TOKEN_PREFIX.length());
            Claims claims = JwtUtil.parseToken(token);
            // 校验token是否过期
            if (JwtUtil.isExpiration(claims)) {
                chain.doFilter(request, response);
                return;
            }
    
            String username = JwtUtil.getUsername(claims);
            if (StringUtils.isBlank(username)) {
                chain.doFilter(request, response);
                return;
            }
            Collection<SimpleGrantedAuthority> authorities = new ArrayList<>();
            for (String authority : JwtUtil.getAuthorities(claims)) {
                authorities.add(new SimpleGrantedAuthority(authority));
            }
            SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(username, null, authorities));
    
            refreshToken(token, response);
            super.doFilterInternal(request, response, chain);
        }
    
        /**
         * 刷新token
         *
         * @param token
         * @param response
         */
        private void refreshToken(String token, HttpServletResponse response) {
            token = JwtUtil.refreshToken(token);
            // 将token添加到header中
            response.setHeader(TOKEN_HEADER, JwtUtil.TOKEN_PREFIX + token);
            // 将token添加到cookie中
            Cookie cookie = new Cookie(TOKEN_HEADER, JwtUtil.TOKEN_PREFIX + token);
            cookie.setPath("/");
            cookie.setMaxAge(JwtUtil.TTL_COOKIE);
            response.addCookie(cookie);
        }
    }
    

    获取客户端令牌兼容了三种方式:

    • cookie中获取令牌,一般用于兼容未做前后端分离的应用
    • header中获取
    • 从请求参数中获取

    令牌验证通过后,解析其用户名和权限,并将其添加至Security上下文中。

    最后刷新令牌,以保持用户长时间活动时其令牌不会过期。

    参考

    1. Spring Security
    2. thymeleaf-extras-springsecurity
    3. Thymeleaf + Spring Security integration basics
    4. Bootstrap4
    5. JWT

    相关文章

      网友评论

        本文标题:SpringBoot 全家桶 | SpringSecurity

        本文链接:https://www.haomeiwen.com/subject/cucaektx.html