Vue.js开发技巧Vue.jsVue.js专区

11、整合JWT

2018-05-18  本文已影响109人  spilledyear

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);
    }
}
上一篇下一篇

猜你喜欢

热点阅读