源码地址https://github.com/Jirath-Liu/shiro-jwt-wx
微信小程序用户登陆,完整流程可参考下面官方地址,本例中是按此流程开发
https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html
你需要了解的点
微信小程序的登录流程
https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html
Shiro的基础知识
https://shiro.apache.org/10-minute-tutorial.html
JWT以及Token
https://jwt.io/introduction/
项目的流程
- 调用 wx.login() 获取 临时登录凭证code ,并回传到开发者服务器。
- 访问login接口,并将code给后台
- 被JwtShirioFilter拦截(在shiro配置中配置的),查看有没有token在Header
- 有则自动执行登录操作,核实token的合法性,并刷新token
- 没有则被controller拦截进入service中进行登录
- 使用code获取用户信息,默认初始化了一些信息(可以修改的)
- 生成token(会存至redis)
- 返回token
本项目的结构
项目分包:
- conf 项目的配置
- exceptoionconfig 配置了异常的抛出,使用@ControllerAdvice进行拦截统一处理
- jwt 包含一个jwt工具类,在使用时会与redis连接,存储、验证与生成token
- shiro 是本项目配置的核心,其中关闭了session管理,使用jwt来完成验证,包含一个自定的应用于shiro的token
- RestTemplateConfig 使用Spring
- enums 包含了需要的枚举类
- vo
- wxapi 包含了一个请求微信后台需要的结果类
不可修改的模块:有JWT与Shiro的类别以及配置模块
具体实现
一、shiro基础配置
1.设置一个自己的realm进行token验证,用户登录会执行你的realm
在DefaultWebSecurityManager 中进行配置
realm是需要自己实现的,先让他在这里报错,当自己的提示也可以
DefaultWebSecurityManager defaultWebSecurityManager=new DefaultWebSecurityManager(tokenRealm);
//设置realm
defaultWebSecurityManager.setRealm(tokenRealm);
2.我们需要关闭shiro的session功能
在DefaultWebSecurityManager 中进行配置
DefaultSubjectDAO subjectDAO = (DefaultSubjectDAO) defaultWebSecurityManager.getSubjectDAO();
DefaultSessionStorageEvaluator evaluator = (DefaultSessionStorageEvaluator) subjectDAO.getSessionStorageEvaluator();
evaluator.setSessionStorageEnabled(Boolean.FALSE)
3.设置realm
我们需要定制一个realm,并且为了能够被识别,选择继承AuthorizingRealm类
需要我们完成的模块:
- realm对请求的识别 我们的方案是验证token是否为我们定制的token
- 审核信息 其中有对token的验证,我们做在工具类中
- 身份/角色验证
- 关闭密码校验加密
@Component
public class TokenRealm extends AuthorizingRealm {
@Autowired
JwtUtil jwtUtil;
/**
* 该方法是为了判断这个主体能否被本Realm处理,判断的方法是查看token是否为同一个类型
* @param authenticationToken
* @return
*/
@Override
public boolean supports(AuthenticationToken authenticationToken) {
return authenticationToken instanceof JwtShiroToken;
}
/**
* 在需要验证身份进行登录时,会通过这个接口,调用本方法进行审核,将身份信息返回,有误则抛出异常,在外层拦截
* @param authenticationToken 这里收到的是自定义的token类型,在JwtShiroToken中,自动向上转型。得到的getCredentials为String类型,可以使用toString
* @return
* @throws AuthenticationException token异常,可以细化设置
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) {
String submittedToken=authenticationToken.getCredentials().toString();
//解析出信息
String wxOpenId = jwtUtil.getWxOpenIdByToken(submittedToken);
String sessionKey = jwtUtil.getSessionKeyByToken(submittedToken);
String userId=jwtUtil.getUserIdByToken(submittedToken);
//对信息进行辨别
if (StringUtils.isEmpty(wxOpenId)) {
throw new TokenException("user account not exits , please check your token");
}
if (StringUtils.isEmpty(sessionKey)) {
throw new TokenException("sessionKey is invalid , please check your token");
}
if (StringUtils.isEmpty(userId)) {
throw new TokenException("userId is invalid , please check your token");
}
if (!jwtUtil.verifyToken(submittedToken)) {
throw new TokenException("token is invalid , please check your token");
}
//在这里将principal换为用户的id
return new SimpleAuthenticationInfo(userId, submittedToken, getName());
}
/**
* 这个方法是用来添加身份信息的,本项目计划为管理员提供网站后台,所以这里不需要身份信息,返回一个简单的即可
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
/**
* 注意坑点 : 密码校验 , 这里因为是JWT形式,就无需密码校验和加密,直接让其返回为true(如果不设置的话,该值默认为false,即始终验证不通过)
*/
@Override
public CredentialsMatcher getCredentialsMatcher() {
return (token, info) -> true;
}
}
4.定制一个token,继承AuthenticationToken
默认的token是包含两个部分的,账号和密码(可以这样理解)
我们将这两个信息都调整为token
/**
* @author Jirath
* @date 2020/4/9
* @description: 一个用于Shiro使用的Authentication,因为使用JWT需要有自己的身份信息,所以使用针对Token定制的信息
*/
@Data
public class JwtShiroToken implements AuthenticationToken {
/**
* 封装,防止误操作
*/
private String token;
/**
* token作为两者进行提交,使用构造方法进行初始化
* @param token 用户提交的token
*/
public JwtShiroToken(String token){
this.token=token;
}
/**
* 在UserNamePasswordToken中,使用的是账号和密码来作为主体和签证,这里我们使用Token登录
* 两者的get都是获取token
*/
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials(