SpringSecurity5 (4) ——集成jwt

JWT是一种用于双方之间传递安全信息的简洁的、URL安全的表述性声明规范。JWT作为一个开放的标准(RFC 7519),定义了一种简洁的,自包含的方法用于通信双方之间以json对象的形式安全的传递信息。因为数字签名的存在,这些信息是可信的,JWT可以使用HMAC算法或者是RSA的公私秘钥对进行签名。

JWT主要包含三个部分之间用英语句号'.'隔开

  1. Header 头部
  2. Payload 负载
  3. Signature 签名

注意,顺序是 header.payload.signature

目前系统开发经常使用前后端分离的模式,前端使用vue等框架,调用后端的rest接口返回json格式的数据,并在前端做展示。登录成功后,后台会向前端返回一个token,前端每次访问后台接口时都携带令牌(在header中携带令牌信息),后台对令牌信息进行校验,如果校验成功可访问后台接口。

(一)实现思路

  1. 我们使用auth0java-jwt是一个JSON WEB TOKEN(JWT)的一个实现;
  2. 登录认证成功后,根据一定的规则生成token,并把token返回给前端;
  3. 增加一个过滤器,对每次请求进行过滤(除登录请求外),查看请求头是否携带有token信息,如果携带有token信息,则对token进行验证,验证通过则进行下一步,验证不通过则返回相应异常信息;前端根据异常信息做出操作。

(二)具体步骤

1、引入依赖

引入com.auth0的依赖,用来生成token信息

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

2、生成token

UserDetailsService的实现类里增加生成token的方法

/**
	 * 保存用户信息
	 * @param userDetails
	 */
	public User saveUserLoginInfo(UserDetails userDetails) {
        /**
				获取用户信息,此处可修改为从redis中获取用户信息
       	*/
		User user = userMapper.getUserByName(userDetails.getUsername());
		if (user != null) {
			String salt = user.getSalt();
			Date loginTime = user.getLastLogin();
			// 挑出部分用户信息,生成token
			User tokenUser = new User();
			tokenUser.setId(user.getId());
			tokenUser.setName(user.getName());

			// 如果需要重复登陆保持 token
			boolean useOldToken = false;

			// 需要重复登陆保持 token,但没有登陆过,或上次登陆已过期,生成新的 salt 与 loginTime,生成新的 token
			if (!useOldToken) {
				salt = BCrypt.gensalt();
				loginTime = new Date();
				user.setSalt(salt);
				user.setLastLogin(loginTime);
				userMapper.updateSalt(user.getId(), salt);
			}
			// 生成token
			Algorithm algorithm = Algorithm.HMAC256(salt);
			Date expiresTime = expiresTime(loginTime);
            //使用jwt的API生成token
			String token = JWT.create()
					//面向用户的值
					.withSubject(JsonUtil.toJson(tokenUser)).
					//过期时间
							withExpiresAt(expiresTime)
					//签发时间
					.withIssuedAt(loginTime)
					//签名算法
					.sign(algorithm);
			log.info("JWT Token is generated at {} for user {}, and will be expired at {}", df.format(loginTime), user.getName(), df.format(expiresTime));
			// 添加或更新缓存 可在此处把用户信息更新或添加到缓存中
			user.setToken(token);
			return user;
		}
		return null;
	}

	private Date expiresTime(Date time) {
		Calendar expiresTime = Calendar.getInstance();
		expiresTime.setTime(time);
		expiresTime.add(Calendar.SECOND, 3600);
		return expiresTime.getTime();
	}

3、增加过滤器、token校验

新开发一个过滤器,对请求进行拦截并验证token,如果token没问题,则放行,如果token异常则返回异常信息给前端。

新增加token校验的服务,对token进行解析及验证是否有效、是否过期。

public class JWTFilter extends OncePerRequestFilter {
	private RequestMatcher requiresAuthenticationRequestMatcher;
	private List<RequestMatcher> permissiveRequestMatchers;

	private TokenService tokenService;

	private SecurityUserDetailsService userMapper;

	private AuthenticationSuccessHandler successHandler ;
	private AuthenticationFailureHandler failureHandler = new AuthFailureHandler();


	public JWTFilter(TokenService tokenService, SecurityUserDetailsService userMapper) {

		this.requiresAuthenticationRequestMatcher =
				new RequestHeaderRequestMatcher("Authorization");
		this.tokenService=tokenService;

		this.userMapper=userMapper;
	}

	@Override
	public void afterPropertiesSet() {

	}

	@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
		if (!requiresAuthentication(request, response)) {
			filterChain.doFilter(request, response);
			return;
		}

		String uri = request.getRequestURI();
		// 不拦截登陆
		if ( "/login".equals(uri)) {
			filterChain.doFilter(request, response);
			return;
		}


		// 从请求头中获取token
		String token = request.getHeader("Authorization");

		//把token解析为jwt对象
		DecodedJWT jwt   = tokenService.decode(token);

		//从数据库或缓存中获取user对象
		User user =tokenService.retrieve(jwt);


		// 退出时只检验 token 的合法性,是否能解析出来user对象
		if ("/logout".equals(uri)) {
			try {
				 tokenService.analytic(token);
				// 上下文中缓存用户
			} catch (Exception e) {
				unsuccessfulAuthentication(request, response, new InternalAuthenticationServiceException("", e));
			}
			filterChain.doFilter(request, response);
			return;
		}

		//查询用户权限生成Authentication对象,这里直接写静态代码,项目中需要从db中查找用户相应的角色
		List<GrantedAuthority> authorities = new ArrayList<>();
		authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
		authorities.add(new SimpleGrantedAuthority("ROLE_ROOT"));
		Authentication authResult = new UsernamePasswordAuthenticationToken(user.getName(),user.getPassword(),authorities);
        
		AuthenticationException failed = null;
     
		try {
			tokenService.validate(token);
		} catch (Exception e) {
			failed = new InternalAuthenticationServiceException("", e);
		}
		if (failed == null) {
			successfulAuthentication(request, response, filterChain,  authResult,token);
		} else if (!permissiveRequest(request)) {
			unsuccessfulAuthentication(request, response, failed);
			return;
		}
		filterChain.doFilter(request, response);
	}


	protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
		// token 校验失败
		failureHandler.onAuthenticationFailure(request, response, failed);
	}

	protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult,String token) throws IOException, ServletException {
		/**
		 *验证成功可以根据业务需求做一系列操作后,请求继续往下进
		 * 比如把用户信息放入threadlocal中,供后续操作使用1
		 */
		SecurityContextHolder.getContext().setAuthentication(authResult);
		//根据用户名查询user对象
		//获取token
		DecodedJWT jwt   = tokenService.decode(token);
		//判断是否应该刷新token
		if(shouldTokenRefresh(jwt.getExpiresAt())){
			User user =userMapper.saveUserLoginInfo((UserDetails) authResult.getPrincipal());
			String newToken =user.getToken();
			response.setHeader("Authorization", newToken);
		}
	}


	protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) {
		return requiresAuthenticationRequestMatcher.matches(request);
	}

	protected boolean permissiveRequest(HttpServletRequest request) {
		if (permissiveRequestMatchers == null) {
			return false;
		}
		for (RequestMatcher permissiveMatcher : permissiveRequestMatchers) {
			if (permissiveMatcher.matches(request)) {
				return true;
			}
		}
		return false;
	}

	/**
	 * 判断是否应该刷新token
	 * @param expireAt
	 * @return
	 */
	protected boolean shouldTokenRefresh(Date expireAt) {
		LocalDateTime expireTime = LocalDateTime.ofInstant(expireAt.toInstant(), ZoneId.systemDefault());
		LocalDateTime freshTime = expireTime.minusSeconds(1200);
		//		log.info("Check token refresh, token will expire at {}, need refresh after {}", expireTime.format(dtf), freshTime.format(dtf));
		return LocalDateTime.now().isAfter(freshTime);
	}
@Component
public class TokenService {

	@Autowired
	private UserMapper userMapper;

	private Logger log = LoggerFactory.getLogger(TokenService.class);

	/**
	 * 只单纯解码 token,取出其中的用户信息
	 * 
	 * @param token
	 * @return
	 */
	public DecodedJWT decode(String token) {
		if (token == null) {
			throw  new RuntimeException("用户未验证");
		}

		DecodedJWT jwt = null;
		try {
			jwt = JWT.decode(token);
		} catch (JWTDecodeException e1) {
			log.warn("Jwt decode token failed, msg is: {}", e1.getLocalizedMessage());
			throw  new RuntimeException("token解析错误");
		}
		return jwt;
	}

	/**
	 * 从 jwt 中解析出用户信息
	 * 
	 * @param token
	 * @return
	 */
	public User analytic(String token) {
		return analytic(decode(token));
	}

	/**
	 * 从 jwt 中解析出用户信息
	 * 
	 * @param jwt
	 * @return
	 */
	public User analytic(DecodedJWT jwt) {
		User user = null;
		try {
			user = JsonUtil.toObject(jwt.getSubject(), User.class);
		} catch (Exception e) {
			log.warn("Jwt subject convert to User failed, msg is: {}", e.getLocalizedMessage());
			throw  new RuntimeException("用户未认证");
		}
		return user;
	}

	/**
	 * 解码 token,并从缓存中或者数据库中取回用户的详细信息
	 * @param jwt
	 * @return
	 */
	public User retrieve(DecodedJWT jwt) {
		User user = null;
		try {
			user = userMapper.getUserByName(analytic(jwt).getName());
		} catch (Exception e) {
			log.warn("Retrieve user from redis cache failed, msg is: {}", e.getLocalizedMessage());
		}
		if (user == null) {
			throw new RuntimeException("用户未登录");
		}
		return user;
	}

	/**
	 * 校验 token 是否合法
	 * @param token
	 * @return
	 */
	public void validate(String token) {
		validate(decode(token), null);
	}

	/**
	 * 校验 token 是否合法
	 * 
	 * @param jwt
	 * @param cofUser 从缓存中可以取得
	 * @return
	 */
	public void validate(DecodedJWT jwt, User user) {
		// 是否超时
		if (Calendar.getInstance().getTime().after(jwt.getExpiresAt())) {
			throw new RuntimeException("token验证失败");
		}
		// 取用户
		if (user == null) {
			user = retrieve(jwt);
		}
		if (user == null) {
			throw new RuntimeException("token验证失败");
		}
		// 用户中不含 salt
		if (user.getSalt() == null) {
			throw new RuntimeException("token验证失败");
		}
		// 校验用户状态, 只有为 enabled 的用户才允许登陆
		if ("ENABLED".equals(user.getStatus())) {
			throw new RuntimeException("token验证失败");
		}
		// 校验token是否合法
		String encryptSalt = user.getSalt();
		try {
			Algorithm algorithm = Algorithm.HMAC256(encryptSalt);
			JWTVerifier verifier = JWT.require(algorithm).withSubject(jwt.getSubject()).build();
			verifier.verify(jwt.getToken());
		} catch (Exception e) {
			log.warn("Jwt verifier token failed, msg is: {}", e.getLocalizedMessage());
			throw new RuntimeException("token验证失败");
		}
	}
}

4、修改配置类

 
@Override
    protected void configure(HttpSecurity http) throws Exception {
        http.addFilterBefore(imageCodeFilter, UsernamePasswordAuthenticationFilter.class)
                .addFilterAfter(getJwtFilter(),ImageCodeFilter.class) //在imageCodeFilter后面加JwtFilter
                .authorizeRequests()
                .antMatchers("/imageCode").permitAll()
                .antMatchers("/hello/admin").hasRole("ROOT")
                .antMatchers("/hello").hasRole("USER").anyRequest().permitAll()
                .and()
                .csrf().disable().
                formLogin().loginPage("/login")  //自定义登录页面跳转
                .defaultSuccessUrl("/hello")
                .successForwardUrl("/hello/admin")//登录成功后跳转
                .successHandler(authSuccessHandler)
                .failureHandler(authFailureHandler)
                .and().httpBasic().disable()
                .sessionManagement().disable()
                .cors()
                .and()
                .logout().logoutUrl("/logout").addLogoutHandler(authLogoutHandler);
    }

    /**
     * 加密方式  配置对token的验证过滤器
     * @return
     */
    @Bean
    protected JWTFilter  getJwtFilter(){
        return new JWTFilter(tokenService,securityUserDetailsService);
    }
原文地址:https://www.cnblogs.com/quartz/p/13372536.html