zoukankan      html  css  js  c++  java
  • 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);
        }
    
  • 相关阅读:
    Maven 入门 (1)—— 安装
    centos下保留python2安装python3
    chrome各版本下载
    nginx 图片访问404 (使用location中使用 root,alias的区别)
    centos7无GUI运行selenium chromedriver 亲测可用!
    常用xpath选择器和css选择器总结
    在flask中使用swagger(flasgger使用方法及效果展示)
    判断回文字符串、回文链表、回文数(python实现)
    (9) MySQL主主复制架构使用方法
    (10) 如何MySQL读压力大的问题
  • 原文地址:https://www.cnblogs.com/quartz/p/13372536.html
Copyright © 2011-2022 走看看