引言
在一些老項目中,可能使用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
如有問題,歡迎添加個人微信討論
歡迎訂閱個人微信公衆號