• Spring Security5整合JWT认证和授权


    JWT介绍

    JWT原理

    JWT是JSON Web Token的缩写,是目前最流行的跨域认证解决方法。

    互联网服务认证的一般流程是:

    1. 用户向服务器发送账号、密码
    2. 服务器验证通过后,将用户的角色、登录时间等信息保存到当前会话中
    3. 同时,服务器向用户返回一个session_id(一般保存在cookie里)
    4. 用户再次发送请求时,把含有session_id的cookie发送给服务器
    5. 服务器收到session_id,查找session,提取用户信息

    上面的认证模式,存在以下缺点:

    • cookie不允许跨域
    • 因为每台服务器都必须保存session对象,所以扩展性不好

    JWT认证原理是:

    1. 用户向服务器发送账号、密码
    2. 服务器验证通过后,生成token令牌返回给客户端(token可以包含用户信息)
    3. 用户再次请求时,把token放到请求头Authorization
    4. 服务器收到请求,验证token合法后放行请求

    JWT token令牌可以包含用户身份、登录时间等信息,这样登录状态保持者由服务器端变为客户端,服务器变成无状态了;token放到请求头,实现了跨域

    JWT数据结构

    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
    

    JWT由三部分组成:

    • Header(头部)
    • Payload(负载)
    • Signature(签名)

    表现形式为:Header.Payload.Signature

    Header 部分是一个 JSON 对象,描述 JWT 的元数据,通常是下面的样子:

    {
      "alg": "HS256",
      "typ": "JWT"
    }
    

    上面代码中,alg属性表示签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256);typ属性表示这个令牌(token)的类型(type),JWT 令牌统一写为JWT

    上面的 JSON 对象使用 Base64URL 算法转成字符串

    Payload

    Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。JWT 规定了7个官方字段:

    • iss (issuer):签发人

    • exp (expiration time):过期时间

    • sub (subject):主题

    • aud (audience):受众

    • nbf (Not Before):生效时间

    • iat (Issued At):签发时间

    • jti (JWT ID):编号

    当然,用户也可以定义私有字段。

    这个 JSON 对象也要使用 Base64URL 算法转成字符串

    Signature

    Signature 部分是对前两部分的签名,防止数据篡改

    签名算法如下:

    HMACSHA256(
      base64UrlEncode(header) + "." +
      base64UrlEncode(payload),
      your-256-bit-secret
    )
    

    算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"."分隔

    JWT认证和授权

    Security是基于AOP和Servlet过滤器的安全框架,为了实现JWT要重写那些方法、自定义那些过滤器需要首先了解security自带的过滤器。security默认过滤器链如下:

    1. org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter
    2. org.springframework.security.web.context.SecurityContextPersistenceFilter
    3. org.springframework.security.web.header.HeaderWriterFilter
    4. org.springframework.security.web.authentication.logout.LogoutFilter
    5. org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
    6. org.springframework.security.web.savedrequest.RequestCacheAwareFilter
    7. org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter
    8. org.springframework.security.web.authentication.AnonymousAuthenticationFilter
    9. org.springframework.security.web.session.SessionManagementFilter
    10. org.springframework.security.web.access.ExceptionTranslationFilter
    11. org.springframework.security.web.access.intercept.FilterSecurityInterceptor

    SecurityContextPersistenceFilter

    这个过滤器有两个作用:

    • 用户发送请求时,从session对象提取用户信息,保存到SecurityContextHolder的securitycontext中
    • 当前请求响应结束时,把SecurityContextHolder的securitycontext保存的用户信息放到session,便于下次请求时共享数据;同时将SecurityContextHolder的securitycontext清空

    由于禁用session功能,所以该过滤器只剩一个作用即把SecurityContextHolder的securitycontext清空。举例来说明为何要清空securitycontext:用户1发送一个请求,由线程M处理,当响应完成线程M放回线程池;用户2发送一个请求,本次请求同样由线程M处理,由于securitycontext没有清空,理应储存用户2的信息但此时储存的是用户1的信息,造成用户信息不符

    UsernamePasswordAuthenticationFilter

    UsernamePasswordAuthenticationFilter继承自AbstractAuthenticationProcessingFilter,处理逻辑在doFilter方法中:

    1. 当请求被UsernamePasswordAuthenticationFilter拦截时,判断请求路径是否匹配登录URL,若不匹配继续执行下个过滤器;否则,执行步骤2
    2. 调用attemptAuthentication方法进行认证。UsernamePasswordAuthenticationFilter重写了attemptAuthentication方法,负责读取表单登录参数,委托AuthenticationManager进行认证,返回一个认证过的token(null表示认证失败)
    3. 判断token是否为null,非null表示认证成功,null表示认证失败
    4. 若认证成功,调用successfulAuthentication。该方法把认证过的token放入securitycontext供后续请求授权,同时该方法预留一个扩展点(AuthenticationSuccessHandler.onAuthenticationSuccess方法),进行认证成功后的处理
    5. 若认证失败,同样可以扩展uthenticationFailureHandler.onAuthenticationFailure进行认证失败后的处理
    6. 只要当前请求路径匹配登录URL,那么无论认证成功还是失败,当前请求都会响应完成,不再执行过滤器链

    UsernamePasswordAuthenticationFilterattemptAuthentication方法,执行逻辑如下:

    1. 从请求中获取表单参数。因为使用HttpServletRequest.getParameter方法获取参数,它只能处理Content-Type为application/x-www-form-urlencoded或multipart/form-data的请求,若是application/json则无法获取值
    2. 把步骤1获取的账号、密码封装成UsernamePasswordAuthenticationToken对象,创建未认证的token。UsernamePasswordAuthenticationToken有两个重载的构造方法,其中public UsernamePasswordAuthenticationToken(Object principal, Object credentials)创建未经认证的token,public UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities)创建已认证的token
    3. 获取认证管理器AuthenticationManager,其缺省实现为ProviderManager,调用其authenticate进行认证
    4. ProviderManagerauthenticate是个模板方法,它遍历所有AuthenticationProvider,直至找到支持认证某类型token的AuthenticationProvider,调用AuthenticationProvider.authenticate方法认证,AuthenticationProvider.authenticate加载正确的账号、密码进行比较验证
    5. AuthenticationManager.authenticate方法返回一个已认证的token

    AnonymousAuthenticationFilter

    AnonymousAuthenticationFilter负责创建匿名token:

    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
            if (SecurityContextHolder.getContext().getAuthentication() == null) {
                SecurityContextHolder.getContext().setAuthentication(this.createAuthentication((HttpServletRequest)req));
                if (this.logger.isTraceEnabled()) {
                    this.logger.trace(LogMessage.of(() -> {
                        return "Set SecurityContextHolder to " + SecurityContextHolder.getContext().getAuthentication();
                    }));
                } else {
                    this.logger.debug("Set SecurityContextHolder to anonymous SecurityContext");
                }
            } else if (this.logger.isTraceEnabled()) {
                this.logger.trace(LogMessage.of(() -> {
                    return "Did not set SecurityContextHolder since already authenticated " + SecurityContextHolder.getContext().getAuthentication();
                }));
            }
    
            chain.doFilter(req, res);
        }
    

    如果当前用户没有认证,会创建一个匿名token,用户是否能读取资源交由FilterSecurityInterceptor过滤器委托给决策管理器判断是否有权限读取

    实现思路

    JWT认证思路:

    1. 利用Security原生的表单认证过滤器验证用户名、密码
    2. 验证通过后自定义AuthenticationSuccessHandler认证成功处理器,由该处理器生成token令牌

    JWT授权思路:

    1. 使用JWT目的是让服务器变成无状态,不用session共享数据,所以要禁用security的session功能(http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS))
    2. token令牌数据结构设计时,payload部分要储存用户名、角色信息
    3. token令牌有两个作用:
      1. 认证, 用户发送的token合法即代表认证成功
      2. 授权,令牌验证成功后提取角色信息,构造认证过的token,将其放到securitycontext,具体权限判断交给security框架处理
    4. 自己实现一个过滤器,拦截用户请求,实现(3)中所说的功能

    代码实现

    创建JWT工具类

    JWT的Java实现,利用开源的java-jwt

    <dependency>
    	<groupId>com.auth0</groupId>
    	<artifactId>java-jwt</artifactId>
    	<version>3.12.0</version>
     </dependency>
    

    我们对java-jwt提供的API进行封装,便于创建、验证、提取claim

    @Slf4j
    public class JWTUtil {
        // 携带token的请求头名字
        public final static String TOKEN_HEADER = "Authorization";
        //token的前缀
        public final static String TOKEN_PREFIX = "Bearer ";
        // 默认密钥
        public final static String DEFAULT_SECRET = "mySecret";
        // 用户身份
        private final static String ROLES_CLAIM = "roles";
        // token有效期,单位分钟;
        private final static long EXPIRE_TIME = 5 * 60 * 1000;
        // 设置Remember-me功能后的token有效期
        private final static long EXPIRE_TIME_REMEMBER = 7 * 24 * 60 * 60 * 1000;
    
        // 创建token
        public static String createToken(String username, List role, String secret, boolean rememberMe) {
    
            Date expireDate = rememberMe ? new Date(System.currentTimeMillis() + EXPIRE_TIME_REMEMBER) : new Date(System.currentTimeMillis() + EXPIRE_TIME);
            try {
                // 创建签名的算法实例
                Algorithm algorithm = Algorithm.HMAC256(secret);
                String token = JWT.create()
                        .withExpiresAt(expireDate)
                        .withClaim("username", username)
                        .withClaim(ROLES_CLAIM, role)
                        .sign(algorithm);
                return token;
            } catch (JWTCreationException jwtCreationException) {
                log.warn("Token create failed");
                return null;
            }
        }
    
        // 验证token
        public static boolean verifyToken(String token, String secret) {
            try{
                Algorithm algorithm = Algorithm.HMAC256(secret);
                // 构建JWT验证器,token合法同时pyload必须含有私有字段username且值一致
                // token过期也会验证失败
                JWTVerifier verifier = JWT.require(algorithm)
                        .build();
                // 验证token
                DecodedJWT decodedJWT = verifier.verify(token);
                return true;
            } catch (JWTVerificationException jwtVerificationException) {
                log.warn("token验证失败");
                return false;
            }
    
        }
    
        // 获取username
        public static String getUsername(String token) {
            try {
                // 因此获取载荷信息不需要密钥
                DecodedJWT jwt = JWT.decode(token);
                return jwt.getClaim("username").asString();
            } catch (JWTDecodeException jwtDecodeException) {
                log.warn("提取用户姓名时,token解码失败");
                return null;
            }
        }
    
        public static List<String> getRole(String token) {
            try {
                // 因此获取载荷信息不需要密钥
                DecodedJWT jwt = JWT.decode(token);
                // asList方法需要指定容器元素的类型
                return jwt.getClaim(ROLES_CLAIM).asList(String.class);
            } catch (JWTDecodeException jwtDecodeException) {
                log.warn("提取身份时,token解码失败");
                return null;
            }
        }
    }
    

    认证

    验证账号、密码交给UsernamePasswordAuthenticationFilter,不用修改代码

    认证成功后,需要生成token返回给客户端,我们通过扩展AuthenticationSuccessHandler.onAuthenticationSuccess方法实现

    @Component
    public class JWTAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
        @Override
        public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
            ResponseData responseData = new ResponseData();
            responseData.setCode("200");
            responseData.setMessage("登录成功!");
    		
            // 提取用户名,准备写入token
            String username = authentication.getName();
            // 提取角色,转为List<String>对象,写入token
            List<String> roles = new ArrayList<>();
            Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
            for (GrantedAuthority authority : authorities){
                roles.add(authority.getAuthority());
            }
    		
            // 创建token
            String token = JWTUtil.createToken(username, roles, JWTUtil.DEFAULT_SECRET, true);
            httpServletResponse.setCharacterEncoding("utf-8");
            // 为了跨域,把token放到响应头WWW-Authenticate里
            httpServletResponse.setHeader("WWW-Authenticate", JWTUtil.TOKEN_PREFIX + token);
    		// 写入响应里
            ObjectMapper mapper = new ObjectMapper();
            mapper.writeValue(httpServletResponse.getWriter(), responseData);
        }
    }
    

    为了统一返回值,我们封装了一个ResponseData对象

    授权

    自定义一个过滤器JWTAuthorizationFilter,验证token,token验证成功后认为认证成功

    @Slf4j
    public class JWTAuthorizationFilter extends OncePerRequestFilter {
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
    
            String token = getTokenFromRequestHeader(request);
            Authentication verifyResult = verefyToken(token, JWTUtil.DEFAULT_SECRET);
            if (verifyResult == null) {
                // 即便验证失败,也继续调用过滤链,匿名过滤器生成匿名令牌
                chain.doFilter(request, response);
                return;
            } else {
                log.info("token令牌验证成功");
                SecurityContextHolder.getContext().setAuthentication(verifyResult);
                chain.doFilter(request, response);
            }
        }
    	
        // 从请求头获取token
        private String getTokenFromRequestHeader(HttpServletRequest request) {
            String header = request.getHeader(JWTUtil.TOKEN_HEADER);
            if (header == null || !header.startsWith(JWTUtil.TOKEN_PREFIX)) {
                log.info("请求头不含JWT token, 调用下个过滤器");
                return null;
            }
    
            String token = header.split(" ")[1].trim();
            return token;
        }
    	
        // 验证token,并生成认证后的token
        private UsernamePasswordAuthenticationToken verefyToken(String token, String secret) {
            if (token == null) {
                return null;
            }
    		
            // 认证失败,返回null
            if (!JWTUtil.verifyToken(token, secret)) {
                return null;
            }
    
            // 提取用户名
            String username = JWTUtil.getUsername(token);
            // 定义权限列表
            List<GrantedAuthority> authorities = new ArrayList<>();
            // 从token提取角色
            List<String> roles = JWTUtil.getRole(token);
            for (String role : roles) {
                log.info("用户身份是:" + role);
                authorities.add(new SimpleGrantedAuthority(role));
            }
            // 构建认证过的token
            return new UsernamePasswordAuthenticationToken(username, null, authorities);
        }
    }
    

    OncePerRequestFilter保证当前请求中,此过滤器只被调用一次,执行逻辑在doFilterInternal

    security配置

    @Configuration
    @EnableWebSecurity
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Autowired
        private AjaxAuthenticationEntryPoint ajaxAuthenticationEntryPoint;
        @Autowired
        private JWTAuthenticationSuccessHandler jwtAuthenticationSuccessHandler;
    
        @Autowired
        private AjaxAuthenticationFailureHandler ajaxAuthenticationFailureHandler;
    
        @Bean
        public PasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }
    
        protected void configure(HttpSecurity http) throws Exception {
            http.csrf().disable()
                    .authorizeRequests().anyRequest().authenticated()
                    .and()
                    .formLogin()
                    .successHandler(jwtAuthenticationSuccessHandler)
                    .failureHandler(ajaxAuthenticationFailureHandler)
                    .permitAll()
                    .and()
                    .addFilterAfter(new JWTAuthorizationFilter(), UsernamePasswordAuthenticationFilter.class)
                    .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                    .and()
                    .exceptionHandling().authenticationEntryPoint(ajaxAuthenticationEntryPoint);
    
        }
    }
    
    

    配置里取消了session功能,把我们定义的过滤器添加到过滤链中;同时,定义ajaxAuthenticationEntryPoint处理未认证用户访问未授权资源时抛出的异常

    @Component
    public class AjaxAuthenticationEntryPoint implements AuthenticationEntryPoint {
        @Override
        public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
            ResponseData responseData = new ResponseData();
            responseData.setCode("401");
            responseData.setMessage("匿名用户,请先登录再访问!");
    
            httpServletResponse.setCharacterEncoding("utf-8");
            ObjectMapper mapper = new ObjectMapper();
            mapper.writeValue(httpServletResponse.getWriter(), responseData);
        }
    }
    

    参考

    JSON Web Token 入门教程

    Spring Security-5-认证流程梳理

    Spring Security3源码分析(5)-SecurityContextPersistenceFilter分析

    Spring Security addFilter() 顺序问题

    前后端联调之Form Data与Request Payload,你真的了解吗?

    Spring Boot 2 + Spring Security 5 + JWT 的单页应用 Restful 解决方案

    SpringBoot实战派-第十章源码

  • 相关阅读:
    iOS 微信支付SDK与微信友盟分享两者同时集成时,出现的问题与解决之路。
    Object-C语言Block的实现方式
    使用Mac命令别名,提升工作效率
    利用OC对象的消息重定向forwardingTargetForSelector方法构建高扩展性的滤镜功能
    渐变色进度条的两种绘制方案
    设计模式应用场景之Model设计中可以用到的设计模式
    有趣的赫夫曼树
    技术团队管理者的问题视角
    SSH安全登陆原理:密码登陆与公钥登陆
    为什么HashMap继承了AbstractMap还要实现Map?
  • 原文地址:https://www.cnblogs.com/weixia-blog/p/14191109.html
Copyright © 2020-2023  润新知