11、整合JWT

作者: spilledyear | 来源:发表于2018-05-18 13:25 被阅读109次

    vue-admin
    vue-monitor

    JWT是JSON Web Token的缩写,即JSON Web令牌。JSON Web令牌(JWT)是一种紧凑的、URL安全的方式,用来表示要在双方之间传递的“声明”。JWT中的声明被编码为JSON对象,用作JSON Web签名(JWS)结构的有效内容或JSON Web加密(JWE)结构的明文,使得声明能够被:数字签名、或利用消息认证码(MAC)保护完整性、加密。

    JWT构成

    一个JWT实际上就是一个字符串,它由三部分组成,头部、载荷与 签名依顺序用点号(".")链接而成:headerpayloadsignature

    Header

    头部(Header)里面说明类型和使用的算法,比如:

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

    说明是JWT(JSON web token)类型,使用了HMAC SHA 算法。然后将头部进行base64加密(该加密是可以对称解密的),构成了第一部分.

    eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
    

    Payload

    载荷(Payload)载荷就是存放有效信息的地方,含三个部分:
    1、标准中注册的声明
    2、公共的声明
    3、私有的声明

    1、标准中注册的声明 (建议但不强制使用) :
    iss: jwt签发者
    sub: jwt所面向的用户
    aud: 接收jwt的一方
    exp: jwt的过期时间,这个过期时间必须要大于签发时间
    nbf: 定义在什么时间之前,该jwt都是不可用的.
    iat: jwt的签发时间
    jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。

    2 、公共的声明
    公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.

    3、私有的声明
    私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。
    定义一个payload:

    {
      "sub": "1234567890",
      "name": "John Doe",
      "admin": true
    }
    

    然后将其进行base64加密,得到Jwt的第二部分。

    eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
    

    把场景2的操作描述成一个json对象。其中添加了一些其他的信息,帮助今后收到这个JWT的服务器理解这个JWT。 当然,你还可以往载荷放非敏感的用户信息,比如uid

    signature

    这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。

    var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);
    
    var signature = HMACSHA256(encodedString, 'secret');
    //TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
    

    注意secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。

    将这三部分用.连接成一个完整的字符串,构成了最终的jwt:

    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
    

    应用场景

    应用场景:
    1、浏览器将用户名和密码以post请求的方式发送给服务器。

    2、服务器接受后验证通过,用一个密钥生成一个JWT。

    3、服务器将这个生成的JWT返回给浏览器。

    4、浏览器存储JWT并在使用时将JWT包含在authorization header里面,然后发送请求给服务器。

    5、服务器可以在JWT中提取用户相关信息。进行验证。

    6、服务器验证完成后,发送响应结果给浏览器。

    好吹就是无状态,在前后端分离的应用中,后台不需要存储状态,减轻服务器的压力。

    整合

    这个又是借鉴了github上一位大神的代码,其实我在好几个地方看大了那个代码了,需要引入一个jar,用于处理JWT的一些操作

            <!-- JWT支持 -->
            <!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
            <dependency>
                <groupId>io.jsonwebtoken</groupId>
                <artifactId>jjwt</artifactId>
                <version>0.9.0</version>
            </dependency>
    

    整体思路如下:
    在 UsernamePasswordAuthenticationFilter 过滤器前添加一个过滤器,该过滤器主要用是针对 JWT 的认证?就是说,当前端的请求头中,包含了token信息,就通过自定义过滤器逻辑走认证过程,认证 通过之后,把认证信息交给spring-security上下文,然后继续走spring-security的流程,就相当于如果前端请求头总有token信息,并且后台校验这个token信息是没有问题的,就让spring-security认为这个请求时已经通过校验的,很巧妙。如果没有包含token,就执行 spring-security标准的认证流程。然后开放一个用于前端请求token的接口,这个接口不需要认证,认证通过之后,返回前端一个token,前端可以保存到 localstorage里面,也可以保存到cookie里面。然后下次请求的时候,在请求头中带上这个token信息。

    下面主要时贴一下核心代码,项目源码都已经上传了,可以自己看你看。
    //security配置类

    package com.hand.sxy.config;
    
    import com.hand.sxy.jwt.JwtAuthenticationEntryPoint;
    import com.hand.sxy.jwt.JwtAuthorizationTokenFilter;
    import com.hand.sxy.jwt.JwtTokenUtil;
    import com.hand.sxy.security.CustomUserService;
    import com.hand.sxy.security.MyFilterSecurityInterceptor;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.core.annotation.Order;
    import org.springframework.http.HttpMethod;
    import org.springframework.security.authentication.AuthenticationManager;
    import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.builders.WebSecurity;
    import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.config.http.SessionCreationPolicy;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.crypto.factory.PasswordEncoderFactories;
    import org.springframework.security.crypto.password.PasswordEncoder;
    import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
    import org.springframework.security.web.authentication.RememberMeServices;
    import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
    import org.springframework.session.security.web.authentication.SpringSessionRememberMeServices;
    import org.springframework.web.cors.CorsConfiguration;
    import org.springframework.web.cors.CorsConfigurationSource;
    import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
    
    import java.util.Arrays;
    
    /**
     * @author spilledyear
     * @date 2018/4/24 13:19
     */
    @Configuration
    @EnableWebSecurity
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Autowired
        private JwtAuthenticationEntryPoint unauthorizedHandler;
    
        @Autowired
        private JwtTokenUtil jwtTokenUtil;
    
        @Value("${jwt.header}")
        private String tokenHeader;
    
        @Value("${jwt.route.authentication.path}")
        private String authenticationPath;
    
        @Autowired
        private MyFilterSecurityInterceptor myFilterSecurityInterceptor;
    
    
        /**
         * 通过这种方式注入 authenticationManagerBean ,然后在别的地方也可以用
         *
         * @return
         * @throws Exception
         */
        @Bean
        @Override
        public AuthenticationManager authenticationManagerBean() throws Exception {
            return super.authenticationManagerBean();
        }
    
        /**
         * 注册 UserDetailsService 的 Bean
         *
         * @return
         */
        @Bean
        UserDetailsService customUserService() {
            return new CustomUserService();
        }
    
        /**
         * Sring5 中密码加密新方式
         *
         * @return
         */
        @Bean
        PasswordEncoder passwordEncoder() {
            return PasswordEncoderFactories.createDelegatingPasswordEncoder();
        }
    
    //    @Bean
    //    RememberMeServices rememberMeServices() {
    //        SpringSessionRememberMeServices rememberMeServices = new SpringSessionRememberMeServices();
    //        rememberMeServices.setAlwaysRemember(true);
    //        return rememberMeServices;
    //    }
    
    //    @Bean
    //    CorsConfigurationSource corsConfigurationSource() {
    //        CorsConfiguration configuration = new CorsConfiguration();
    //        configuration.setAllowedOrigins(Arrays.asList("http://localhost:8082"));
    //        configuration.addAllowedHeader("*");
    //        configuration.setAllowedMethods(Arrays.asList("POST, GET, OPTIONS, PUT, DELETE"));
    //        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    //        source.registerCorsConfiguration("/**", configuration);
    //        return source;
    //    }
    
    
        @Override
        protected void configure(HttpSecurity httpSecurity) throws Exception {
    //        httpSecurity.rememberMe().rememberMeServices(rememberMeServices());
    //        httpSecurity.addFilterBefore(myFilterSecurityInterceptor, FilterSecurityInterceptor.class);
    
            httpSecurity
                    .cors().and()
                    .csrf().disable()
                    .exceptionHandling().authenticationEntryPoint(unauthorizedHandler)
    
                    /** 不创建 session **/
                    .and()
                    .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
    
                    .and()
                    .authorizeRequests()
                    .antMatchers("/*.html", "/**/*.html", "/**/*.js", "/**/*.css").permitAll()
                    .antMatchers("/login", "/register", "/auth", "/oauth/*").permitAll()
                    .antMatchers("/api/role/query").hasRole("ADMIN")
                    .anyRequest().authenticated()
    
                    .and()
                    .formLogin().loginPage("/login").loginProcessingUrl("/api/system/login").usernameParameter("username").passwordParameter("password").permitAll()
    
                    .and()
                    .logout().logoutUrl("/logout").logoutSuccessUrl("/api/system/logout").permitAll();
    
    
            /**
             * spring security过滤器链中,真正的用户信息校验是 UsernamePasswordAuthenticationFilter 过滤器,然后才是权限校验。
             * 这里在 UsernamePasswordAuthenticationFilter过滤器之前 自定义一个过滤器,这样就可以提前根据token将authenticate信息
             * 维护进speing security上下文,然后在 UsernamePasswordAuthenticationFilter 得到的就已经是通过校验的用户了。
             */
            JwtAuthorizationTokenFilter authenticationTokenFilter = new JwtAuthorizationTokenFilter(customUserService(), jwtTokenUtil, tokenHeader);
            httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
    
            /**
             * disable page caching
             *
             * 下面这行代码巨玄乎,加了这个之后,前端应用就无法正常访问了(也就是说需要开发/api/**权限才能正常 访问)
             */
    //        httpSecurity.headers().frameOptions().sameOrigin().cacheControl();
    
        }
    
        @Autowired
        public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
            /**
             * userDetailService验证
             */
    //        auth.userDetailsService(customUserService()).passwordEncoder(new PasswordEncoder() {
    //
    //            @Override
    //            public String encode(CharSequence rawPassword) {
    //                return rawPassword.toString();
    //            }
    //
    //            @Override
    //            public boolean matches(CharSequence rawPassword, String encodedPassword) {
    //                return encodedPassword.equals(rawPassword.toString());
    //            }
    //        });
    
            auth.userDetailsService(customUserService()).passwordEncoder(passwordEncoder());
        }
    
        @Override
        public void configure(WebSecurity webSecurity) {
            webSecurity
                    .ignoring().antMatchers(HttpMethod.POST, "/login", "/auth")
    
                    .and()
                    .ignoring().antMatchers("/**/*.html", "/**/*.js", "/**/*.css");
        }
    }
    

    注意上面的

    .exceptionHandling().authenticationEntryPoint(unauthorizedHandler)
    
    package com.hand.sxy.jwt;
    
    import org.springframework.security.core.AuthenticationException;
    import org.springframework.security.web.AuthenticationEntryPoint;
    import org.springframework.stereotype.Component;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.io.Serializable;
    
    @Component
    public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint, Serializable {
    
        private static final long serialVersionUID = -8970718410437077606L;
    
        @Override
        public void commence(HttpServletRequest request,
                             HttpServletResponse response,
                             AuthenticationException authException) throws IOException {
            // This is invoked when user tries to access a secured REST resource without supplying any credentials
            // We should just send a 401 Unauthorized response because there is no 'login page' to redirect to
            response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
        }
    }
    

    这表示当你权限认证失败的时候,执行 JwtAuthenticationEntryPoint 里面的 commence方法,返回给前端,用于自定义适合客户阅读的提示。

    这个就是开放的接口,用于前端请求token信息。

        /**
         * 认证接口,用于前端获取 JWT 的接口
         *
         * @param user
         * @return
         * @throws AuthenticationException
         */
        @RequestMapping(value = "${jwt.route.authentication.path}", method = RequestMethod.POST)
        @ResponseBody
        public ResultResponse obtainToken(@RequestBody User user) throws AuthenticationException {
    
            /**
             * 通过调用 spring security 中的 authenticationManager 对用户进行验证
             */
            Objects.requireNonNull(user.getUsername());
            Objects.requireNonNull(user.getPassword());
            try {
                authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword()));
            } catch (DisabledException e) {
                throw new AuthenticationException("该已被被禁用,请检查", e);
            } catch (BadCredentialsException e) {
                throw new AuthenticationException("无效的密码,请检查", e);
            }
    
            /**
             * 根据用户名从数据库获取用户信息,然后生成 token
             */
            final UserDetails userDetails = userDetailsService.loadUserByUsername(user.getUsername());
            final String token = jwtTokenUtil.generateToken(userDetails);
    
            List<User> userList = userService.query(user);
            ResultResponse resultSet = new ResultResponse(true, token);
            resultSet.setRows(userList);
            return resultSet;
        }
    

    //这个时jwt工具类

    package com.hand.sxy.jwt;
    
    import com.hand.sxy.security.CustomUser;
    import io.jsonwebtoken.Claims;
    import io.jsonwebtoken.Clock;
    import io.jsonwebtoken.Jwts;
    import io.jsonwebtoken.SignatureAlgorithm;
    import io.jsonwebtoken.impl.DefaultClock;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.stereotype.Component;
    
    import java.io.Serializable;
    import java.util.Date;
    import java.util.HashMap;
    import java.util.Map;
    import java.util.function.Function;
    
    /**
     * @author spilledyear
     */
    @Component
    public class JwtTokenUtil implements Serializable {
        private static final long serialVersionUID = -3301605591108950415L;
    
        private Clock clock = DefaultClock.INSTANCE;
    
        @Value("${jwt.secret}")
        private String secret;
    
        @Value("${jwt.expiration}")
        private Long expiration;
    
        public String getUsernameFromToken(String token) {
            return getClaimFromToken(token, Claims::getSubject);
        }
    
        public Date getIssuedAtDateFromToken(String token) {
            return getClaimFromToken(token, Claims::getIssuedAt);
        }
    
        public Date getExpirationDateFromToken(String token) {
            return getClaimFromToken(token, Claims::getExpiration);
        }
    
        public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
            final Claims claims = getAllClaimsFromToken(token);
            return claimsResolver.apply(claims);
        }
    
        private Claims getAllClaimsFromToken(String token) {
            return Jwts.parser()
                    .setSigningKey(secret)
                    .parseClaimsJws(token)
                    .getBody();
        }
    
        private Boolean isTokenExpired(String token) {
            final Date expiration = getExpirationDateFromToken(token);
            return expiration.before(clock.now());
        }
    
        private Boolean isCreatedBeforeLastPasswordReset(Date created, Date lastPasswordReset) {
            return (lastPasswordReset != null && created.before(lastPasswordReset));
        }
    
        private Boolean ignoreTokenExpiration(String token) {
            // here you specify tokens, for that the expiration is ignored
            return false;
        }
    
        public String generateToken(UserDetails userDetails) {
            Map<String, Object> claims = new HashMap<>();
            return doGenerateToken(claims, userDetails.getUsername());
        }
    
        private String doGenerateToken(Map<String, Object> claims, String subject) {
            final Date createdDate = clock.now();
            final Date expirationDate = calculateExpirationDate(createdDate);
    
            return Jwts.builder()
                    .setClaims(claims)
                    .setSubject(subject)
                    .setIssuedAt(createdDate)
                    .setExpiration(expirationDate)
                    .signWith(SignatureAlgorithm.HS512, secret)
                    .compact();
        }
    
        public Boolean canTokenBeRefreshed(String token, Date lastPasswordReset) {
            final Date created = getIssuedAtDateFromToken(token);
            return !isCreatedBeforeLastPasswordReset(created, lastPasswordReset)
                    && (!isTokenExpired(token) || ignoreTokenExpiration(token));
        }
    
        public String refreshToken(String token) {
            final Date createdDate = clock.now();
            final Date expirationDate = calculateExpirationDate(createdDate);
    
            final Claims claims = getAllClaimsFromToken(token);
            claims.setIssuedAt(createdDate);
            claims.setExpiration(expirationDate);
    
            return Jwts.builder()
                    .setClaims(claims)
                    .signWith(SignatureAlgorithm.HS512, secret)
                    .compact();
        }
    
        public Boolean validateToken(String token, UserDetails userDetails) {
            CustomUser customUser = (CustomUser) userDetails;
            final String username = getUsernameFromToken(token);
            final Date created = getIssuedAtDateFromToken(token);
            //final Date expiration = getExpirationDateFromToken(token);
            return (
                    username.equals(customUser.getUsername()) && !isTokenExpired(token) && !isCreatedBeforeLastPasswordReset(created, customUser.getLastPasswordResetDate())
            );
        }
    
        private Date calculateExpirationDate(Date createdDate) {
            return new Date(createdDate.getTime() + expiration * 1000);
        }
    }
    

    //这个就是关键的过滤器了

    package com.hand.sxy.jwt;
    
    import io.jsonwebtoken.ExpiredJwtException;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
    import org.springframework.security.core.context.SecurityContextHolder;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
    import org.springframework.web.filter.OncePerRequestFilter;
    
    import javax.servlet.FilterChain;
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    
    
    /**
     * @author spilledyear
     */
    public class JwtAuthorizationTokenFilter extends OncePerRequestFilter {
    
        private final Logger logger = LoggerFactory.getLogger(this.getClass());
    
        private UserDetailsService userDetailsService;
        private JwtTokenUtil jwtTokenUtil;
        private String tokenHeader;
    
        public JwtAuthorizationTokenFilter(UserDetailsService userDetailsService, JwtTokenUtil jwtTokenUtil, String tokenHeader) {
            this.userDetailsService = userDetailsService;
            this.jwtTokenUtil = jwtTokenUtil;
            this.tokenHeader = tokenHeader;
        }
    
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
            logger.debug("processing authentication for '{}'", request.getRequestURL());
    
            final String token = request.getHeader(this.tokenHeader);
    
            String username = null;
            if (token != null && !"".equals(token)) {
                try {
                    username = jwtTokenUtil.getUsernameFromToken(token);
                } catch (IllegalArgumentException e) {
                    logger.error("从Token中获取用户名失败", e);
                } catch (ExpiredJwtException e) {
                    logger.warn("这个Token已经失效了", e);
                }
            } else {
                logger.warn("请求头中未发现 Token, 将执行Spring Security正常的验证流程");
            }
    
            if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                logger.debug("security context was null, so authorizating user");
    
                // 也可以将用户信息保存在token中,这时候就可以不用查数据库
                UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
    
                // 校验前端传过来的Token是否有问题
                if (jwtTokenUtil.validateToken(token, userDetails)) {
                    UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                    authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    logger.info("用户 '{}' 授权成功, 赋值给 SecurityContextHolder 上下文", username);
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                }
            }
    
            chain.doFilter(request, response);
        }
    }
    

    相关文章

      网友评论

        本文标题:11、整合JWT

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