在 shiro 基础上整合 jwt,可在项目中直接使用呦
- 2022 年 6 月 24 日
本文字数:6300 字
阅读完需:约 21 分钟
前几天给大家讲解了一下 shiro,后台一些小伙伴跑来给我留言说:“一般不都是 shiro 结合 jwt 做身份和权限验证吗?能不能再讲解一下 jwt 的用法呢?“今天阿 Q 就给大家讲一下 shiro 整合 jwt 做权限校验吧。
首先呢我还是要说一下 jwt 的概念:JWT 全称 Json web token , 是为了在网络应用环境间传递声明而执行的一种基于 JSON 的开放标准。该 token 被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT 的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该 token 也可直接被用于认证,也可被加密。通俗点说呢,就是之前的 session 为了区分是哪个用户发来的请求,需要在服务端存储用户信息,需要消耗服务器资源。并且随着用户量的增大,势必会扩展服务器,采用分布式系统,这样的话 session 就可能就不太合适了,而我们今天说的 jwt 呢就很好地解决了单点登录问题,很容易的解决 session 共享的问题。
话不多说,直接上整合教程(本期是在上期 shiro 的基础上进行的改造):
一、在 pom 文件中引入 jwt 的依赖包
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.7.0</version>
</dependency>
二、写一个工具类用于生成签名和验证签名
public class JwtUtil {
//JWT-account
public static final String ACCOUNT = "username";
//JWT-currentTimeMillis
public final static String CURRENT_TIME_MILLIS = "currentTimeMillis";
//有效期时间2小时
public static final long EXPIRE_TIME = 2 * 60 * 60 * 1000L;
//秘钥
public static final String SECRET_KEY = "shirokey";
/**
* 生成签名返回token
*
* @param account
* @param currentTimeMillis
* @return
*/
public static String sign(String account, String currentTimeMillis) {
// 帐号加JWT私钥加密
String secret = account + SECRET_KEY;
// 此处过期时间,单位:毫秒,在当前时间到后边的20分钟内都是有效的
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
//采用HMAC256加密
Algorithm algorithm = Algorithm.HMAC256(secret);
return JWT.create()
.withClaim(ACCOUNT, account)
.withClaim(CURRENT_TIME_MILLIS, currentTimeMillis)
.withExpiresAt(date)
//创建一个新的JWT,并使用给定的算法进行标记
.sign(algorithm);
}
/**
* 校验token是否正确
*
* @param token
* @return
*/
public static boolean verify(String token) {
String secret = getClaim(token, ACCOUNT) + SECRET_KEY;
Algorithm algorithm = Algorithm.HMAC256(secret);
JWTVerifier verifier = JWT.require(algorithm)
.build();
verifier.verify(token);
return true;
}
/**
* 获得Token中的信息无需secret解密也能获得
*
* @param token
* @param claim
* @return
*/
public static String getClaim(String token, String claim) {
try {
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim(claim).asString();
} catch (JWTDecodeException e) {
return null;
}
}
}
三、封装自己的 token,用于后边校验 token 类型
public class JwtToken implements AuthenticationToken {
private final String token;
public JwtToken(String token){
this.token = token;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
四、我们需要在登陆时创建 token
//service中的登录处理
@Override
public UserTokenDTO login(UserTokenDTO userInfo) {
// 从数据库获取对应用户名密码的用户
SysUserInfo uInfo = userInfoMapper.getUserByLogin(userInfo.getName());
if (null == uInfo) {
//用户信息不存在
throw new BusinessException(CommonResultStatus.USERNAME_ERROR);
} else if (!userInfo.getPassword().equals(uInfo.getPassword())) {
//密码错误
throw new BusinessException(CommonResultStatus.PASSWORD_ERROR);
}
//生成jwtToken
userInfo.setToken(JwtUtil.sign(userInfo.getName(),String.valueOf(System.currentTimeMillis())));
return userInfo;
}
五、在其他需要登录后才能访问的请求中解析 token,所以我们要自定义过滤器
public class JwtFilter extends AccessControlFilter {
//设置请求头中需要传递的字段名
protected static final String AUTHORIZATION_HEADER = "Access-Token";
/**
* 表示是否允许访问,mappedValue就是[urls]配置中拦截器参数部分,
* 如果允许访问返回true,否则false
* @author cheetah
* @date 2020/11/24
* @param request:
* @param response:
* @param mappedValue:
* @return: boolean
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
return false;
}
/**
* 表示当访问拒绝时是否已经处理了,
* 如果返回true表示需要继续处理,
* 如果返回false表示该拦截器实例已经处理了,将直接返回即可
* @author cheetah
* @date 2020/11/24
* @param request:
* @param response:
* @return: boolean
*/
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest req = (HttpServletRequest) request;
// 解决跨域问题
if(HttpMethod.OPTIONS.toString().matches(req.getMethod())) {
return true;
}
if (isLoginAttempt(request, response)) {
//生成jwt token
JwtToken token = new JwtToken(req.getHeader(AUTHORIZATION_HEADER));
//委托给Realm进行验证
try {
//调用登陆会走Realm中的身份验证方法
getSubject(request, response).login(token);
return true;
} catch (Exception e) {
}
}else{
throw new BusinessException(CommonResultStatus.LOGIN_ERROR);
}
return false;
}
/**
* 判断是否有头部参数
* @author cheetah
* @date 2020/11/24
* @param request:
* @param response:
* @return: boolean
*/
protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
HttpServletRequest req = (HttpServletRequest) request;
String authorization = req.getHeader(AUTHORIZATION_HEADER);
return authorization != null;
}
}
六、当滤器中调用 subject.login(token)方法时,会走自定义 Realm 中的 doGetAuthenticationInfo(AuthenticationToken token)方法来验证身份
@Slf4j
public class JwtRealm extends AuthorizingRealm {
@Autowired
private UserInfoService userService;
//验证是不是自己的token类型
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}
/**
* 身份验证
* @author cheetah
* @date 2020/11/25
* @param token:
* @return: org.apache.shiro.authc.AuthenticationInfo
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String credentials = (String) token.getCredentials();
String username = null;
try {
//jwt验证token
boolean verify = JwtUtil.verify(credentials);
if (!verify) {
throw new AuthenticationException("Token校验不正确");
}
username = JwtUtil.getClaim(credentials, JwtUtil.ACCOUNT);
} catch (Exception e) {
throw new BusinessException(CommonResultStatus.TOKEN_CHECK_ERROR,e.getMessage());
}
//交给AuthenticatingRealm使用CredentialsMatcher进行密码匹配,不设置则使用默认的SimpleCredentialsMatcher
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
username, //用户名
credentials, //凭证
getName() //realm name
);
return authenticationInfo;
}
/**
* 权限校验(次数不做过多讲解)
* @author cheetah
* @date 2020/11/25
* @param principals:
* @return: org.apache.shiro.authz.AuthorizationInfo
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// String username = principals.toString();
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
//角色权限暂时不加
// authorizationInfo.setRoles(userService.getRoles(username));
// authorizationInfo.setStringPermissions(userService.queryPermissions(username));
return authorizationInfo;
}
}
七、接下来我们需要修改 ShiroConfig 文件,将自定义的 Filter 和 Realm 交由 SecurityManager 进行管理
/**
此类较长,只展示部分重要代码,其余代码可在公众号“阿Q说”中回复"jwt"获取源码
**/
@Configuration
@Slf4j
public class ShiroConfig {
/**
* 创建ShiroFilterFactoryBean
* @author cheetah
* @date 2020/11/21
* @return: org.apache.shiro.spring.web.ShiroFilterFactoryBean
*/
@Bean
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 必须设置 SecurityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
//设置shiro内置过滤器
Map<String, Filter> filters = new LinkedHashMap<>();
//添加自定义过滤器:只对需要登陆的接口进行过滤
filters.put("authc", new JwtFilter());
//添加自定义过滤器:对权限进行验证
// filters.put("roles", new CustomRolesOrAuthorizationFilter());
shiroFilterFactoryBean.setFilters(filters);
// setLoginUrl 如果不设置值,默认会自动寻找Web工程根目录下的"/login.jsp"页面 或 "/login" 映射
shiroFilterFactoryBean.setLoginUrl("/adminLogin/login");
// 设置无权限时跳转的 url;
shiroFilterFactoryBean.setUnauthorizedUrl("/notAuth");
// 设置拦截器
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
//游客,开发权限
filterChainDefinitionMap.put("/guest/**", "anon");
//用户,需要角色权限 “user”
filterChainDefinitionMap.put("/user/**", "roles[user]");
// filterChainDefinitionMap.put("/productInfo/**", "roles[user]");
//管理员,需要角色权限 “admin”
filterChainDefinitionMap.put("/admin/**", "roles[admin]");
//开放登陆接口
filterChainDefinitionMap.put("/adminLogin/login", "anon");
//其余接口一律拦截
//主要这行代码必须放在所有权限设置的最后,不然会导致所有 url 都被拦截
filterChainDefinitionMap.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
log.info("-------Shiro拦截器工厂类注入成功-----------");
return shiroFilterFactoryBean;
}
/**
* 注入安全管理器
* @author cheetah
* @date 2020/11/21
* @return: java.lang.SecurityManager
*/
@Bean
public DefaultWebSecurityManager securityManager(JwtRealm jwtRealm, SubjectFactory subjectFactory,
SessionManager sessionManager,
CacheManager cacheManager) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(jwtRealm);
//关闭shiro自带的session
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
securityManager.setSubjectDAO(subjectDAO);
securityManager.setSubjectFactory(subjectFactory);
securityManager.setSessionManager(sessionManager);
securityManager.setCacheManager(cacheManager);
return securityManager;
}
/**
* jwt身份认证和权限校验
* @author cheetah
* @date 2020/11/24
* @return: com.cheetah.shiroandjwt.jwt.JwtRealm
*/
@Bean
public JwtRealm jwtRealm() {
JwtRealm jwtRealm = new JwtRealm();
jwtRealm.setAuthenticationCachingEnabled(true);
jwtRealm.setAuthorizationCachingEnabled(true);
return jwtRealm;
}
}
重点:将自定义的 Realm 交由 SecurityManage 管理,关闭 shiro 自带的 session
接下来我们启动成程序验证一下:当我们未登录时,请求失败,需要先登录
登录成功获取 token 信息
当带着头部信息"Access-Token"访问时就可以获取信息了。
回复“jwt”可获取本期源码!
阿 Q 将持续更新 java 实战方面的文章,如果你有不同的意见或者更好的 idea,欢迎联系阿 Q。
【阿 Q 说代码】,值得关注的公众号
文章风格多变,配图通俗易懂,故事生动有趣,来聊聊技术呀!
版权声明: 本文为 InfoQ 作者【阿Q说代码】的原创文章。
原文链接:【http://xie.infoq.cn/article/c44de97f4624a6c2dcc8b6d38】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
阿Q说代码
公众号:阿Q说代码 | 🏆 签约作者 🏆 2021.06.08 加入
目前就职于世界五百强企业公司,担任技术leader,文章风格多变,配图通俗易懂,故事生动有趣!
评论