引言
在一些老项目中,可能使用shiro进行权限认证和校验,而shiro是基于cookie/session的, 在现在前后端分离开发的场景下,前端开发人员需要在本地和后端进行调试,势必会遇到跨域的问题。 而现在随着谷歌浏览器升级,已经禁止在跨域的情况下携带cookie。
所以,基于此背景,对老项目的shiro框架进行一番改造,使得支持token的认证方式,又不影响旧代码。
在进行Shiro 改造之前,首先我们得先了解下Shiro的基本机制。这里主要涉及到4个组件:
AuthenticationToken
身份验证的抽象接口,该接口会返回2个信息,用户信息(Principal)和凭证信息(Credentials)CredentilsMatcher
凭证校验接口,其实就是密码的校验器AuthenticatingFilter
身份验证过滤器,在请求过来的时候创建AuthenticationToken
对象以及执行登录操作AuthorizingRealm
授权接口,在该接口会获取权限信息(doGetAuthorizationInfo)和用户信息(doGetAuthenticationInfo)
基本流程如下:
改造思路
- 创建一个
AuthenticationToken
接口的实现类,用于存放我们的Token信息 - 创建一个
AuthenticatingFilter
的实现类,这里我们需要做3件事- 当前Ruequest获取token,从而创建
AuthenticationToken
对象 - 在
onAccessDenied
方法,校验Token的有效性 - 最后执行登录操作,这里的登录操作其实是用token换取用户信息,会执行
AuthorizingRealm
的doGetAuthenticationInfo
方法
- 当前Ruequest获取token,从而创建
- 创建一个
AuthorizingRealm
的实现类,这里主要做3件事- 重写
supports
方法,使得支持我们自定义的Token - 实现
doGetAuthorizationInfo
方法,这里是返回用户的权限集合 - 实现
doGetAuthenticationInfo
方法,这里我们根据Token获取用户信息
- 重写
- 创建一个
CredentilsMatcher
接口的实现类,这里我们不对密码进行校验,直接返回true。因为当你能拿到Token,证明账号密码已经校验过。所以账号密码校验实际应该是在业务层进行校验,校验通过之后才创建Token
代码实现
AuthenticationToken
的实现类
public class ShiroToken implements AuthenticationToken {
private String token;
public ShiroToken(String token) {
this.token = token;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
复制代码
AuthenticatingFilter
的实现类
@Slf4j
public class TokenFilter extends AuthenticatingFilter {
private static final String X_TOKEN = "X-Token";
private ITokenService tokenService = null;
/**
* 创建Token, 支持自定义Token
*/
@Override
protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
String token = this.getToken((HttpServletRequest)servletRequest);
if(ObjectUtils.isEmpty(token)){
log.error("token is empty");
return null;
}
return new ShiroToken(token);
}
/**
* 兼容跨域
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
return ((HttpServletRequest) request).getMethod().equals(RequestMethod.OPTIONS.name());
}
@Override
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
HttpServletRequest request = (HttpServletRequest)servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
String token = this.getToken(request);
if(ObjectUtils.isEmpty(token)){
this.respUnLogin(request, response);
return false;
}
// 校验Token的有效性
if(tokenService == null){
tokenService = SpringContext.getBean(ITokenService.class);
}
if(!tokenService.check(token)){
this.respUnLogin(request, response);
}
// 根据token获取用户信息,会执行 TokenRealm#doGetAuthenticationInfo 方法
return executeLogin(servletRequest, servletResponse);
}
private void respUnLogin(HttpServletRequest request, HttpServletResponse response) throws IOException {
response.setContentType("application/json;charset=utf-8");
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin"));
Response resp = new Response(BusinessCodeEnum.USER_UN_LOGIN.getCode(), BusinessCodeEnum.USER_UN_LOGIN.getMsg());
response.getWriter().print(JSONUtil.toJsonStr(resp));
}
/**
* 获取token
* 优先从header获取
* 如果没有,则从parameter获取
* @param request request
* @return token
*/
private String getToken(HttpServletRequest request){
String token = request.getHeader(X_TOKEN);
if(ObjectUtils.isEmpty(token)){
token = request.getParameter(X_TOKEN);
}
return token;
}
}
复制代码
这里需要特别注意
TokenFilter
是Filter
的实现类,并不在Spring的容器中管理,所以无法通过@Autowire
等注解进行注入,只能通过构造函数或者在使用的时候通过Spring的上下文中获取。建议通过Spring的上下文中获取,通过构造器注入可能采坑。
AuthorizingRealm
的实现类
@Slf4j
public class TokenRealm extends AuthorizingRealm {
@Autowired
@Lazy
private ITokenService tokenService;
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof ShiroToken;
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
log.info("user do authorization: {}", principalCollection);
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
log.info("user do authentication: {}", authenticationToken);
ShiroToken token = (ShiroToken)authenticationToken;
UserInfo userInfo = tokenService.getUserInfo(token.getCredentials().toString());
if(userInfo == null){
throw BusinessCodeEnum.TOKEN_INVALID.getException();
}
return new SimpleAuthenticationInfo(userInfo.getUsername(), userInfo.getPassword(), userInfo.getNickName());
}
}
复制代码
CredentilsMatcher
的实现类
public class TokenCredentialsMatcher implements CredentialsMatcher {
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
return token instanceof ShiroToken;
}
}
复制代码
最后
通过以上的简单改造,我们就可以实现基于Token的认证方式又不需要改动Shiro框架的其他功能了。
完整源码地址:shiro-to-token
如有问题,欢迎添加个人微信讨论
欢迎订阅个人微信公众号