前言
大家好,一直以来我都本着用最通俗的话理解核心的知识点, 我认为所有的难点都离不开 基础知识 的铺垫。目前正在出一个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包的部署与服务启动, 还将会带大家如何搭建测试环境和线上环境。欢迎加群一起学习交流 ~