此前阻桅,我寫過一篇《我是怎么做App Token認證的》 的文章凉倚,使用的是PHP技術(shù),這次將本人的SpringBoot版本分享一下嫂沉,其中使用了Apache的Shiro作為權(quán)限管理組件稽寒。老規(guī)矩,關(guān)于基礎(chǔ)的知識比如Shiro的原理及應用趟章,同學們請自行百度杏糙。
在這里,我簡單介紹一下我是怎么具體實現(xiàn)的蚓土,重點描述結(jié)合Shiro宏侍,如何實現(xiàn)token生成、token校驗及token緩存蜀漆。
生成Token
服務(wù)端接收客戶端傳遞的username和password等請求谅河,在數(shù)據(jù)庫中檢查,如果用戶名密碼匹配的話确丢,表示登錄成功绷耍,服務(wù)端生成并返回一個token訪問令牌。
@PostMapping("/login/wechat")
public R loginWechat(@RequestParam String openid, @RequestParam(required = false) String nickname,
@RequestParam(required = false, defaultValue = "0") int gender, @RequestParam(required = false) String avatar,
HttpServletRequest request) {
Auth auth = new Auth();
auth.setIdentify(openid).setIdentifyType(Constants.AuthType.WECHAT);
auth.setLastLoginTime(LocalDateTime.now());
OAuthUserVO oAuthUserVO = new OAuthUserVO();
oAuthUserVO.setAvatar(avatar);
oAuthUserVO.setGender(gender);
oAuthUserVO.setUsername(nickname);
return loginThird(auth, oAuthUserVO, generateToken(request));
}
private R loginThird(Auth auth, OAuthUserVO oAuthUserVO, UserToken userToken) {
User user = userService.loginThird(auth, oAuthUserVO); // 真正的登錄蠕嫁,數(shù)據(jù)校驗锨天,這里使用第三方登錄,Auth是第三方登錄的信息剃毒。
if (user == null) {
return R.fail("用戶不存在");
}
AuthUser authUser = BeanCopierUtils.copy(user, AuthUser.class);
// 登錄成功病袄,發(fā)放令牌,寫入緩存
authManager.onLoginSuccess(authUser, userToken);
// 通知第三方權(quán)限框架進行認證和授權(quán)
authManager.login(authUser, userToken);
LoginVO data = new LoginVO();
data.setToken(userToken.getToken());
data.setUser(BeanCopierUtils.copy(user, UserVO.class));
return R.ok(data);
}
在上面的Controller代碼中赘阀,登錄成功益缠,服務(wù)端向客戶端返回token和user信息。token是通過AuthManager
類的generateToken
方法+onLoginSucess
來生成的基公。
// 收集客戶端數(shù)據(jù)
public UserToken generateToken(HttpServletRequest request) {
String ip = HttpRequestUtils.getIpAddr(request);
String uuid = HttpRequestUtils.getParam(request, "uuid");
String client = HttpRequestUtils.getParam(request, "client");
UserToken userToken = new UserToken();
userToken.setClient(client);
userToken.setIp(ip);
userToken.setUuid(uuid);
userToken.setCreatedAt(LocalDateTime.now());
return userToken;
}
public void onLoginSuccess(AuthUser authUser, UserToken userToken) {
// 刷新用戶Token
Wrapper<UserToken> wrapper = Wrappers.<UserToken>lambdaQuery().eq(UserToken::getUserId, authUser.getId());
userTokenService.remove(wrapper);
userToken.setUserId(authUser.getId());
// 使用隨機UUID生成一個token
userToken.setToken(CommonUtils.randomUUID());
userTokenService.save(userToken);
// 用戶附帶設(shè)備信息
authUser.additional(userToken);
// token寫入緩存
redisCache.set(userToken.getToken(), authUser, Constants.Second.A_DAY);
}
這里主要有3個步驟
- 收集設(shè)備uuid, ip等信息生成一個UserToken實例幅慌,后續(xù)隨機生成一個字符串作為token令牌
- 根據(jù)用戶id查詢用戶登錄表,不管用戶原來是否已經(jīng)存在token轰豆,新的token將替換舊的token
- 將token寫入緩存胰伍,因為token是每個請求都會解析齿诞,如果不使用緩存的話,會導致數(shù)據(jù)庫訪問瓶頸骂租。
- 用戶的UserToken會附加到AuthUser(作為Shiro的Principal)
客戶端應該保存token祷杈,然后在后續(xù)的訪問中都要帶上這個token(PHP版本中是可選的),后臺配置一個過濾器渗饮,將解析并校驗token的合法性但汞。
解析token
服務(wù)端接收客戶端傳遞的token,需要從中解析出相關(guān)的用戶及設(shè)備信息互站。
@Override
public boolean isAccessAllowed(HttpServletRequest request, HttpServletResponse response) {
String token = HttpRequestUtils.getParam(request, Constants.Http.AUTHORIZE_NAME);
if (!StringUtils.isEmpty(token)) {
Object o = redisCache.get(token);
AuthUser loginUser = (AuthUser)getLoginUser();
UserToken newToken = generateToken(request);
newToken.setToken(token);
return verifyToken((AuthUser)o, loginUser, newToken);
}
return false;
}
過程很簡單私蕾,從請求中獲取token,并生成一份新的UserToken胡桃,再取出緩存和shiro的當前登錄用戶信息踩叭,進入token驗證方法。
驗證token
private boolean loginWithToken(String token) {
Wrapper<UserToken> wrapper = Wrappers.<UserToken>lambdaQuery().eq(UserToken::getToken, token);
UserToken userToken = userTokenService.getOne(wrapper);
if (userToken != null && userToken.isExpired()) {
log.info("AuthFilter 數(shù)據(jù)庫中Token有效");
// TODO 判斷UUID标捺,IP是否一致
User user = userService.getById(userToken.getUserId());
if (user != null) {
log.info("AuthFilter 數(shù)據(jù)庫中用戶有效懊纳,重新登錄并且更新緩存及數(shù)據(jù)庫中的token信息");
userTokenService.updateById(userToken);
AuthUser authUser = BeanCopierUtils.copy(user, AuthUser.class);
// 用戶附帶設(shè)備信息
authUser.additional(userToken);
// token寫入緩存
redisCache.set(userToken.getToken(), authUser, Constants.Second.A_DAY);
// 通知第三方權(quán)限框架進行認證和授權(quán)
login(authUser, userToken);
return true;
}
}
return false;
}
public boolean verifyToken(AuthUser cacheUser, AuthUser loginUser, UserToken comingToken) {
if (loginUser == null) {
log.info("AuthFilter 用戶未登錄,檢查緩存");
if (cacheUser == null) {
log.info("AuthFilter 緩存中沒有Token亡容,檢查數(shù)據(jù)庫");
return loginWithToken(comingToken.getToken());
} else {
log.info("AuthFilter 緩存中有Token");
// TODO 判斷UUID嗤疯,IP是否一致
login(cacheUser, comingToken);
return true;
}
} else {
log.info("AuthFilter 用戶已登錄");
if (loginUser.getToken().equals(comingToken.getToken())) {
log.info("AuthFilter token相同,通過");
return true;
} else {
log.info("AuthFilter token不一致闺兢,先注銷茂缚,再登錄");
logout();
return loginWithToken(comingToken.getToken());
}
}
}
緩存
緩存相對簡單,緩存的key為token值屋谭,也即登錄成功后脚囊,服務(wù)端向客戶端發(fā)送的token值。緩存有效值為1天桐磁,如果token校驗通過悔耘,則應該在更新緩存有效期(上面的代碼中沒有)。
// token寫入緩存
redisCache.set(userToken.getToken(), authUser, Constants.Second.A_DAY);
備注
以上僅為代碼片斷我擂,核心代碼為我對shiro的二次封裝庫衬以。此庫反向依賴于start模塊的IAuthManager實現(xiàn)。
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @author jamling
*/
public interface IAuthManager {
/**
* 登錄校摩,交第三方權(quán)限框架去登錄(認證)
*
* @param user 當前用戶
* @param credentials 憑據(jù)
*/
void login(IAuthUser user, Object credentials);
void logout();
/**
* 獲取當前登錄的用戶信息
*
* @return 用戶信息
*/
IAuthUser getLoginUser();
/**
* 從原始請求中檢查用戶token看峻,由權(quán)限過濾器調(diào)用
*
* @param request http請求
* @param response http響應
* @return 檢查結(jié)果 true表示檢查通過
*/
boolean isAccessAllowed(HttpServletRequest request, HttpServletResponse response);
boolean verifyToken(String token);
/**
* 檢查token不通過,使用何種方式登錄
*
* @param request http請求
* @param response http響應
* @return 處理結(jié)果 true表示已處理衙吩,不會再繼續(xù)提交父類處理
*/
boolean executeLogin(HttpServletRequest request, HttpServletResponse response);
/**
* 第三方權(quán)限框架執(zhí)行授權(quán)
*
* @param principal 當前用戶
* @return
* @see org.apache.shiro.realm.AuthorizingRealm#doGetAuthorizationInfo(org.apache.shiro.subject.PrincipalCollection)
*/
IAuthUser doGetAuthorizationInfo(Object principal);
}