前言
大家好,一直以来我都本着用最通俗的话理解核心的知识点, 我认为所有的难点都离不开 基础知识 的铺垫。目前正在出一个SpringBoot
长期系列教程,从入门到进阶, 篇幅会较多~
适合人群
- 学完Java基础
- 想通过Java快速构建web应用程序
- 想学习或了解SpringBoot
- SpringBoot进阶学习
大佬可以绕过 ~
背景
如果你是一路看过来的,很高兴你能够耐心看完。之前带大家学了Springboot
基础部分,对基本的使用有了初步的认识, 接下来的几期内容将会带大家进阶使用,会先讲解基础中间件
的使用和一些场景的应用,或许这些技术你听说过,没看过也没关系,我会带大家一步一步的入门,耐心看完你一定会有收获
~
情景回顾
上期带大家学习了Shiro
中如何进行缓存以及它的Session会话管理
,还带大家实现了一个在线用户管理的例子,本期将带大家学习Shiro
中如何整合JWT
以及跨域处理
,本篇是这个系列的终极篇, 同样的,我们集成到Springboot
中。
啥是JWT
在实现之前,我们一起来了解一下啥是jwt
。首先它的全称是JSON Web Token
, 它是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全地传输信息。该信息可以被验证和信任,因为它是数字签名
使用场景
授权场景
: 这是使用JWT的最常见场景。一旦用户登录,后续每个请求都将包含JWT,允许用户访问该令牌允许的路由、服务和资源。单点登录是现在广泛使用的JWT的一个特性,因为它的开销很小,并且可以轻松地跨域使用。信息交换
: 对于安全的在各方之间传输信息而言,JSON Web Tokens无疑是一种很好的方式。因为JWT可以被签名,例如,用公钥/私钥对,你可以确定发送人就是它们所说的那个人。另外,由于签名是使用头和有效负载计算的,您还可以验证内容没有被篡改。
工作原理
在认证的时候,当用户用他们的凭证成功登录以后,一个JSON Web Token
将会被返回。此后,token就是用户凭证了,你必须非常小心以防止出现安全问题。一般而言,你保存令牌的时候不应该超过你所需要它的时间。
无论何时用户想要访问受保护的路由或者资源的时候,用户代理(通常是浏览器)都应该带上JWT,典型的,通常放在Authorization header中,用Bearer schema
。
服务器上的受保护的路由将会检查Authorization header
中的JWT是否有效,如果有效,则用户可以访问受保护的资源。如果JWT包含足够多的必需的数据,那么就可以减少
对某些操作的数据库查询的需要。
如果token是在授权头(Authorization header)中发送的,那么跨源资源共享(CORS)
将不会成为问题,因为它不使用cookie。
环境搭建
首先我们要引入相关依赖,在pom.xml
中添加如下:
<!-- jwt --> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.4.1</version> </dependency> 复制代码
添加配置 ShiroConfig
为了不混淆之前的配置,我们新建一个配置,放到authentication
包下, 这里直接贴完整例子,没啥好说的,之前都讲过
@Configuration public class ShiroConfig { private final Logger logger = LoggerFactory.getLogger(this.getClass()); @Value("${spring.redis.host}") private String redisHost; @Value("${spring.redis.port}") private Integer redisPort; private static final Integer expireAt = 1800; private static final Integer timeout = 3000; @Value("${spring.redis.password}") private String redisPassword; @Bean public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); String prefix = "/api"; shiroFilterFactoryBean.setLoginUrl(prefix + "/notLogin"); shiroFilterFactoryBean.setUnauthorizedUrl(prefix + "/notRole"); Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>(); LinkedHashMap<String, Filter> filters = new LinkedHashMap<>(); filters.put("jwt", new JwtFilter()); shiroFilterFactoryBean.setFilters(filters); // 所有请求都要经过 jwt过滤器 filterChainDefinitionMap.put("/**", "jwt"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); logger.warn("Shiro jwt 拦截器工厂类注入成功"); return shiroFilterFactoryBean; } @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) { AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager); return authorizationAttributeSourceAdvisor; } /** * 注入 securityManager */ @Bean public SecurityManager securityManager() { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); // 设置realm. securityManager.setRealm(customRealm()); // 设置缓存 securityManager.setCacheManager(cacheManager()); // 设置会话 securityManager.setSessionManager(sessionManager()); return securityManager; } /** * 自定义身份认证 realm; * <p> * 必须写这个类,并加上 @Bean 注解,目的是注入 CustomRealm, * 否则会影响 CustomRealm类 中其他类的依赖注入 */ @Bean public CustomRealm customRealm() { return new CustomRealm(); } /** * 加入redis缓存,避免重复从数据库获取数据 */ public RedisManager redisManager() { RedisManager redisManager = new RedisManager(); redisManager.setHost(redisHost); redisManager.setPort(redisPort); redisManager.setPassword(redisPassword); redisManager.setExpire(expireAt); redisManager.setTimeout(timeout); return redisManager; } public RedisCacheManager cacheManager() { RedisCacheManager redisCacheManager = new RedisCacheManager(); redisCacheManager.setRedisManager(redisManager()); return redisCacheManager; } /** * session 会话管理 */ @Bean public RedisSessionDAO sessionDAO() { RedisSessionDAO redisSessionDAO = new RedisSessionDAO(); redisSessionDAO.setRedisManager(redisManager()); return redisSessionDAO; } @Bean public SimpleCookie sessionIdCookie(){ SimpleCookie cookie = new SimpleCookie("X-Token"); cookie.setMaxAge(-1); cookie.setPath("/"); cookie.setHttpOnly(false); return cookie; } @Bean public SessionManager sessionManager() { DefaultWebSessionManager sessionManager = new DefaultWebSessionManager(); sessionManager.setSessionIdCookie(sessionIdCookie()); sessionManager.setSessionIdCookieEnabled(true); Collection<SessionListener> listeners = new ArrayList<SessionListener>(); listeners.add(new ShiroSessionListener()); sessionManager.setSessionListeners(listeners); sessionManager.setSessionDAO(sessionDAO()); return sessionManager; } } 复制代码
实现JwtFilter过滤器
实际上核心是实现JwtFilter
这个过滤器, 下面贴个完整案例给大家参考一下:
public class JwtFilter extends BasicHttpAuthenticationFilter { private Logger log = LoggerFactory.getLogger(this.getClass()); private static final String TOKEN = "Authorization"; private AntPathMatcher pathMatcher = new AntPathMatcher(); @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws UnauthorizedException { HttpServletRequest httpServletRequest = (HttpServletRequest) request; // 这里大家可以处理白名单逻辑,这里就不实现了 比如 /login 我们需要放行 // if (match) { // return true; // } if (isLoginAttempt(request, response)) { return executeLogin(request, response); } log.error("未传token {}", httpServletRequest.getRequestURI()); return false; } @Override protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) { HttpServletRequest req = (HttpServletRequest) request; String token = req.getHeader(TOKEN); return token != null; } @Override protected boolean executeLogin(ServletRequest request, ServletResponse response) { HttpServletRequest httpServletRequest = (HttpServletRequest) request; String token = httpServletRequest.getHeader(TOKEN); JwtToken jwtToken = new JwtToken(token); try { getSubject(request, response).login(jwtToken); return true; } catch (Exception e) { request.setAttribute("fail", e.getMessage()); log.error("executeLogin {}", e.getMessage()); return false; } } /** * 对跨域提供支持(注意生产) */ @Override protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception { HttpServletRequest httpServletRequest = (HttpServletRequest) request; HttpServletResponse httpServletResponse = (HttpServletResponse) response; httpServletResponse.setHeader("Access-control-Allow-Origin", "*"); httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS"); httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers")); if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) { httpServletResponse.setStatus(HttpStatus.OK.value()); return false; } return super.preHandle(request, response); } } 复制代码
这一步可以处理白名单,处理跨域~
实现 JwtToken
下面我们开始实现jwt
的逻辑, 首先定义一个实体
public class JwtToken implements AuthenticationToken { private static final long serialVersionUID = 1282057025599826155L; private String token; private String expireAt; public JwtToken(String token) { this.token = token; } public JwtToken(String token, String expireAt) { this.token = token; this.expireAt = expireAt; } @Override public Object getPrincipal() { return token; } @Override public Object getCredentials() { return token; } public String getToken() { return token; } public void setToken(String token) { this.token = token; } public String getExpireAt() { return expireAt; } public void setExpireAt(String expireAt) { this.expireAt = expireAt; } } 复制代码
封装jwt工具类
这里直接给大家封装好,直接用,一般写业务的时候,这种常用的工具最好封装起来,也方便别人使用
public class JwtUtil { private static Logger log = LoggerFactory.getLogger(JwtUtil.class); // 设置过期时间 private static final long EXPIRE_TIME = 1000 * 72 * 36; // 设置秘钥 (这里推荐大家可以写入 yml配置文件里) private static final String Secret = "28ca017de15a57e206f0"; /** * 校验 token是否正确 * * @param token 密钥 * @return 是否正确 */ public static boolean verify(String token, User user) { try { Algorithm algorithm = Algorithm.HMAC256(Secret); JWTVerifier verifier = JWT.require(algorithm) .withClaim("userId", user.getId()) .withClaim("roleId", user.getRole()) .build(); verifier.verify(token); log.info("token is valid"); return true; } catch (Exception e) { log.error("token is invalid{}", e.getMessage()); return false; } } /** * 从 token中获取用户id * * @return token中包含的用户id */ public static String getUserId(String token) { try { DecodedJWT jwt = JWT.decode(token); return jwt.getClaim("userId").asString(); } catch (JWTDecodeException e) { log.error("error:{}", e.getMessage()); return null; } } /** * 从 token中获取用户roleId * * @return token中包含的用户id */ public static Integer getRoleId(String token) { try { DecodedJWT jwt = JWT.decode(token); return jwt.getClaim("roleId").asInt(); } catch (JWTDecodeException e) { log.error("error:{}", e.getMessage()); return null; } } /** * 生成 token * * @param user * @return token */ public static String sign(User user) { try { Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME); // 这里可以加入秘钥 Algorithm algorithm = Algorithm.HMAC256(Secret); // 这里可以存放于jwt中的内容信息,最后可以通过解密拿到 return JWT.create() .withClaim("userId", user.getId()) .withClaim("roleId", user.getRole()) .withExpiresAt(date) .sign(algorithm); } catch (Exception e) { log.error("error:{}", e); return null; } } } 复制代码
相关注释已经写在上面了~
实现验证逻辑
我们知道Shiro
的验证逻辑部分在于我们自己实现的CustomRealm
, 所以下面我们来实现一下它:
public class CustomRealm extends AuthorizingRealm { @Override public boolean supports(AuthenticationToken token) { return token instanceof JwtToken; } /** * 授权模块,获取用户角色和权限 * @param token token * @return AuthorizationInfo 权限信息 */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection token) { SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); String userId = JwtUtil.getUserId(token.toString()); if(userId == null) { return simpleAuthorizationInfo; } String userRole = UserMock.getRoleById(userId); Set<String> role = new HashSet<>(); role.add(userRole); simpleAuthorizationInfo.setRoles(role); simpleAuthorizationInfo.setStringPermissions(role); return simpleAuthorizationInfo; } /** * 用户认证 * * @param authenticationToken 身份认证 token * @return AuthenticationInfo 身份认证信息 * @throws AuthenticationException 认证相关异常 */ @Override protected SimpleAuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { String token = (String) authenticationToken.getCredentials(); String userId = JwtUtil.getUserId(token); if (StringUtils.isBlank(userId)) { throw new AuthenticationException("验证失败"); } String userRole = UserMock.getRoleById(userId); User userBean = new User(); userBean.setUserId(userId); userBean.setRole(userRole); if (!JwtUtil.verify(token, userBean)) { throw new AuthenticationException("token失效"); } return new SimpleAuthenticationInfo(token, token, "shiroJwtRealm"); } } 复制代码
这部分逻辑大家可以根据具体功能自有发挥,方法就那么几个~
如何去验证 & 注意事项
这里就不带大家一一去测试了,留给大家自己去思考。流程给大家简要说一下,首先是用户通过login
,验证成功后,你需要调用jwtUtil
去签发token
给前端,前端拿到后,放入请求头中,这样每次请求都会去携带这个token
,服务端会从请求头中获取这个token
,然后进行验证,验证通过后继续执行,失败就会返回失败信息,这个之前教过大家如何去捕获
。这里需要强调的是token
的刷新机制,因为如果让用户频繁的跳登录这样体验是很不友好的,所以过期时间设置
和刷新机制
这个大家要根据自身业务来定,如何去刷新
,这个需要跟前端同学协商好~
结束语
本期内容就到这里结束了,总结一下,本节主要讲了Shiro
如何进行整合jwt
,大家可以举一反三,做一些小功能尝试尝试
下期预告
其实学到这里,我们去做一些业务基本上没啥太大问题了,有的时候我们写完代码需要我们自己去打包并部署到服务器,一般情况下有专门的同学会做这件事。但是,这里还是教大家一下如何去部署服务,这个技能对于服务端的同学还是要必会的,说不定哪天就是你发的呢,下期就带大家学习如何线上部署,将涉及到nginx
部署教程,以及jar包
的部署与服务启动, 还将会带大家如何搭建测试环境和线上环境。欢迎加群一起学习交流 ~