怎么在SpringBoot和Redis中實現一個Token權限認證功能?針對這個問題,這篇文章詳細介紹了相對應的分析和解答,希望可以幫助更多想解決這個問題的小伙伴找到更簡單易行的方法。
用戶登錄成功后,后臺返回一個token給調用者,同時自定義一個@AuthToken注解,被該注解標注的API請求都需要進行token效驗,效驗通過才可以正常訪問,實現接口級的鑒權控制。
同時token具有生命周期,在用戶持續一段時間不進行操作的話,token則會過期,用戶一直操作的話,則不會過期。
SpringBoot
Redis(Docke中鏡像)
MySQL(Docker中鏡像)
(1)、客戶端登錄,輸入用戶名和密碼,后臺進行驗證,如果驗證失敗則返回登錄失敗的提示。
如果驗證成功,則生成 token 然后將 username 和 token 雙向綁定 (可以根據 username 取出 token 也可以根據 token 取出username)存入redis,同時使用 token+username 作為key把當前時間戳也存入redis。并且給它們都設置過期時間。
(2)、每次請求接口都會走攔截器,如果該接口標注了@AuthToken注解,則要檢查客戶端傳過來的Authorization字段,獲取 token。
由于 token 與 username 雙向綁定,可以通過獲取的 token 來嘗試從 redis 中獲取 username,如果可以獲取則說明 token 正確,反之,說明錯誤,返回鑒權失敗。
(3)、token可以根據用戶使用的情況來動態的調整自己過期時間。
在生成 token 的同時也往 redis 里面存入了創建 token 時的時間戳,每次請求被攔截器攔截 token 驗證成功之后,將當前時間與存在 redis 里面的 token 生成時刻的時間戳進行比較,當當前時間的距離創建時間快要到達設置的redis過期時間的話,就重新設置token過期時間,將過期時間延長。
如果用戶在設置的 redis 過期時間的時間長度內沒有進行任何操作(沒有發請求),則token會在redis中過期。
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface AuthToken {
}@RestController
public class welcome {
Logger logger = LoggerFactory.getLogger(welcome.class);
@Autowired
Md5TokenGenerator tokenGenerator;
@Autowired
UserMapper userMapper;
@GetMapping("/welcome")
public String welcome(){
return "welcome token authentication";
}
@RequestMapping(value = "/login", method = RequestMethod.GET)
public ResponseTemplate login(String username, String password) {
logger.info("username:"+username+" password:"+password);
User user = userMapper.getUser(username,password);
logger.info("user:"+user);
JSONObject result = new JSONObject();
if (user != null) {
Jedis jedis = new Jedis("192.168.1.106", 6379);
String token = tokenGenerator.generate(username, password);
jedis.set(username, token);
//設置key生存時間,當key過期時,它會被自動刪除,時間是秒
jedis.expire(username, ConstantKit.TOKEN_EXPIRE_TIME);
jedis.set(token, username);
jedis.expire(token, ConstantKit.TOKEN_EXPIRE_TIME);
Long currentTime = System.currentTimeMillis();
jedis.set(token + username, currentTime.toString());
//用完關閉
jedis.close();
result.put("status", "登錄成功");
result.put("token", token);
} else {
result.put("status", "登錄失敗");
}
return ResponseTemplate.builder()
.code(200)
.message("登錄成功")
.data(result)
.build();
}
//測試權限訪問
@RequestMapping(value = "test", method = RequestMethod.GET)
@AuthToken
public ResponseTemplate test() {
logger.info("已進入test路徑");
return ResponseTemplate.builder()
.code(200)
.message("Success")
.data("test url")
.build();
}
}@Slf4j
public class AuthorizationInterceptor implements HandlerInterceptor {
//存放鑒權信息的Header名稱,默認是Authorization
private String httpHeaderName = "Authorization";
//鑒權失敗后返回的錯誤信息,默認為401 unauthorized
private String unauthorizedErrorMessage = "401 unauthorized";
//鑒權失敗后返回的HTTP錯誤碼,默認為401
private int unauthorizedErrorCode = HttpServletResponse.SC_UNAUTHORIZED;
//存放登錄用戶模型Key的Request Key
public static final String REQUEST_CURRENT_KEY = "REQUEST_CURRENT_KEY";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (!(handler instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
// 如果打上了AuthToken注解則需要驗證token
if (method.getAnnotation(AuthToken.class) != null || handlerMethod.getBeanType().getAnnotation(AuthToken.class) != null) {
String token = request.getParameter(httpHeaderName);
log.info("Get token from request is {} ", token);
String username = "";
Jedis jedis = new Jedis("192.168.1.106", 6379);
if (token != null && token.length() != 0) {
username = jedis.get(token);
log.info("Get username from Redis is {}", username);
}
if (username != null && !username.trim().equals("")) {
Long tokeBirthTime = Long.valueOf(jedis.get(token + username));
log.info("token Birth time is: {}", tokeBirthTime);
Long diff = System.currentTimeMillis() - tokeBirthTime;
log.info("token is exist : {} ms", diff);
if (diff > ConstantKit.TOKEN_RESET_TIME) {
jedis.expire(username, ConstantKit.TOKEN_EXPIRE_TIME);
jedis.expire(token, ConstantKit.TOKEN_EXPIRE_TIME);
log.info("Reset expire time success!");
Long newBirthTime = System.currentTimeMillis();
jedis.set(token + username, newBirthTime.toString());
}
//用完關閉
jedis.close();
request.setAttribute(REQUEST_CURRENT_KEY, username);
return true;
} else {
JSONObject jsonObject = new JSONObject();
PrintWriter out = null;
try {
response.setStatus(unauthorizedErrorCode);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
jsonObject.put("code", ((HttpServletResponse) response).getStatus());
jsonObject.put("message", HttpStatus.UNAUTHORIZED);
out = response.getWriter();
out.println(jsonObject);
return false;
} catch (Exception e) {
e.printStackTrace();
} finally {
if (null != out) {
out.flush();
out.close();
}
}
}
}
request.setAttribute(REQUEST_CURRENT_KEY, null);
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}

登陸權限控制,實際上利用的就是攔截器的攔截功能。因為每一次請求都要通過攔截器,只有攔截器驗證通過了,才能訪問想要的請求路徑,所以在攔截器中做校驗Token校驗。
想要代碼,可以去GitHub上查看。
https://github.com/Hofanking/token-authentication.git
攔截器介紹,可以參考 這篇文章
補充:springboot+spring security+redis實現登錄權限管理
筆者負責的電商項目的技術體系是基于SpringBoot,為了實現一套后端能夠承載ToB和ToC的業務,需要完善現有的權限管理體系。
在查看Shiro和Spring Security對比后,筆者認為Spring Security更加適合本項目使用,可以總結為以下2點:
1、基于攔截器的權限校驗邏輯,可以針對ToB的業務接口來做相關的權限校驗,以筆者的項目為例,ToB的接口請求路徑以/openshop/api/開頭,可以根據接口請求路徑配置全局的ToB的攔截器;
2、Spring Security的權限管理模型更簡單直觀,對權限、角色和用戶做了很好的解耦。
以下介紹本項目的實現步驟
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> <version>1.5.3.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>4.3.8.RELEASE</version> </dependency>
public abstract class AbstractAuthenticationInterceptor extends HandlerInterceptorAdapter implements InitializingBean {
@Resource
private AccessDecisionManager accessDecisionManager;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//檢查是否登錄
String userId = null;
try {
userId = getUserId();
}catch (Exception e){
JsonUtil.renderJson(response,403,"{}");
return false;
}
if(StringUtils.isEmpty(userId)){
JsonUtil.renderJson(response,403,"{}");
return false;
}
//檢查權限
Collection<? extends GrantedAuthority> authorities = getAttributes(userId);
Collection<ConfigAttribute> configAttributes = getAttributes(request);
return accessDecisionManager.decide(authorities,configAttributes);
}
//獲取用戶id
public abstract String getUserId();
//根據用戶id獲取用戶的角色集合
public abstract Collection<? extends GrantedAuthority> getAttributes(String userId);
//查詢請求需要的權限
public abstract Collection<ConfigAttribute> getAttributes(HttpServletRequest request);
}@Component
public class AuthenticationInterceptor extends AbstractAuthenticationInterceptor {
@Resource
private SessionManager sessionManager;
@Resource
private UserPermissionService customUserService;
@Override
public String getUserId() {
return sessionManager.obtainUserId();
}
@Override
public Collection<? extends GrantedAuthority> getAttributes(String s) {
return customUserService.getAuthoritiesById(s);
}
@Override
public Collection<ConfigAttribute> getAttributes(HttpServletRequest request) {
return customUserService.getAttributes(request);
}
@Override
public void afterPropertiesSet() throws Exception {
}
}集成redis維護用戶session信息
@Component
public class SessionManager {
private static final Logger logger = LoggerFactory.getLogger(SessionManager.class);
@Autowired
private RedisUtils redisUtils;
public SessionManager() {
}
public UserInfoDTO obtainUserInfo() {
UserInfoDTO userInfoDTO = null;
try {
String token = this.obtainToken();
logger.info("=======token=========", token);
if (StringUtils.isEmpty(token)) {
LemonException.throwLemonException(AccessAuthCode.sessionExpired.getCode(), AccessAuthCode.sessionExpired.getDesc());
}
userInfoDTO = (UserInfoDTO)this.redisUtils.obtain(this.obtainToken(), UserInfoDTO.class);
} catch (Exception var3) {
logger.error("obtainUserInfo ex:", var3);
}
if (null == userInfoDTO) {
LemonException.throwLemonException(AccessAuthCode.sessionExpired.getCode(), AccessAuthCode.sessionExpired.getDesc());
}
return userInfoDTO;
}
public String obtainUserId() {
return this.obtainUserInfo().getUserId();
}
public String obtainToken() {
HttpServletRequest request = ((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getRequest();
String token = request.getHeader("token");
return token;
}
public UserInfoDTO createSession(UserInfoDTO userInfoDTO, long expired) {
String token = UUIDUtil.obtainUUID("token.");
userInfoDTO.setToken(token);
if (expired == 0L) {
this.redisUtils.put(token, userInfoDTO);
} else {
this.redisUtils.put(token, userInfoDTO, expired);
}
return userInfoDTO;
}
public void destroySession() {
String token = this.obtainToken();
if (StringUtils.isNotBlank(token)) {
this.redisUtils.remove(token);
}
}
}@Service
public class UserPermissionService {
@Resource
private SysUserDao userDao;
@Resource
private SysPermissionDao permissionDao;
private HashMap<String, Collection<ConfigAttribute>> map =null;
/**
* 加載資源,初始化資源變量
*/
public void loadResourceDefine(){
map = new HashMap<>();
Collection<ConfigAttribute> array;
ConfigAttribute cfg;
List<SysPermission> permissions = permissionDao.findAll();
for(SysPermission permission : permissions) {
array = new ArrayList<>();
cfg = new SecurityConfig(permission.getName());
array.add(cfg);
map.put(permission.getUrl(), array);
}
}
/*
*
* @Author zhangs
* @Description 獲取用戶權限列表
* @Date 18:56 2019/11/11
**/
public List<GrantedAuthority> getAuthoritiesById(String userId) {
SysUserRspDTO user = userDao.findById(userId);
if (user != null) {
List<SysPermission> permissions = permissionDao.findByAdminUserId(user.getUserId());
List<GrantedAuthority> grantedAuthorities = new ArrayList <>();
for (SysPermission permission : permissions) {
if (permission != null && permission.getName()!=null) {
GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(permission.getName());
grantedAuthorities.add(grantedAuthority);
}
}
return grantedAuthorities;
}
return null;
}
/*
*
* @Author zhangs
* @Description 獲取當前請求所需權限
* @Date 18:57 2019/11/11
**/
public Collection<ConfigAttribute> getAttributes(HttpServletRequest request) throws IllegalArgumentException {
if(map !=null) map.clear();
loadResourceDefine();
AntPathRequestMatcher matcher;
String resUrl;
for(Iterator<String> iter = map.keySet().iterator(); iter.hasNext(); ) {
resUrl = iter.next();
matcher = new AntPathRequestMatcher(resUrl);
if(matcher.matches(request)) {
return map.get(resUrl);
}
}
return null;
}
}通過查看authorities中的權限列表是否含有configAttributes中所需的權限,判斷用戶是否具有請求當前資源或者執行當前操作的權限。
@Service
public class AccessDecisionManager {
public boolean decide(Collection<? extends GrantedAuthority> authorities, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
if(null== configAttributes || configAttributes.size() <=0) {
return true;
}
ConfigAttribute c;
String needRole;
for(Iterator<ConfigAttribute> iter = configAttributes.iterator(); iter.hasNext(); ) {
c = iter.next();
needRole = c.getAttribute();
for(GrantedAuthority ga : authorities) {
if(needRole.trim().equals(ga.getAuthority())) {
return true;
}
}
}
return false;
}
}@Configuration
public class WebAppConfigurer extends WebMvcConfigurerAdapter {
@Resource
private AbstractAuthenticationInterceptor authenticationInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 多個攔截器組成一個攔截器鏈
// addPathPatterns 用于添加攔截規則
// excludePathPatterns 用戶排除攔截
//對來自/openshop/api/** 這個鏈接來的請求進行攔截
registry.addInterceptor(authenticationInterceptor).addPathPatterns("/openshop/api/**");
super.addInterceptors(registry);
}
}用戶表 sys_user
CREATE TABLE `sys_user` ( `user_id` varchar(64) NOT NULL COMMENT '用戶ID', `username` varchar(255) DEFAULT NULL COMMENT '登錄賬號', `first_login` datetime(6) NOT NULL COMMENT '首次登錄時間', `last_login` datetime(6) NOT NULL COMMENT '上次登錄時間', `pay_pwd` varchar(100) DEFAULT NULL COMMENT '支付密碼', `chant_id` varchar(64) NOT NULL DEFAULT '-1' COMMENT '關聯商戶id', `create_time` datetime DEFAULT NULL COMMENT '創建時間', `modify_time` datetime DEFAULT NULL COMMENT '修改時間', PRIMARY KEY (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
角色表 sys_role
CREATE TABLE `sys_role` ( `role_id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(255) DEFAULT NULL, `create_time` datetime DEFAULT NULL COMMENT '創建時間', `modify_time` datetime DEFAULT NULL COMMENT '修改時間', PRIMARY KEY (`role_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
用戶角色關聯表 sys_role_user
CREATE TABLE `sys_role_user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `sys_user_id` varchar(64) DEFAULT NULL, `sys_role_id` int(11) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
權限表 sys_premission
CREATE TABLE `sys_permission` ( `permission_id` int(11) NOT NULL, `name` varchar(255) DEFAULT NULL COMMENT '權限名稱', `description` varchar(255) DEFAULT NULL COMMENT '權限描述', `url` varchar(255) DEFAULT NULL COMMENT '資源url', `check_pwd` int(2) NOT NULL DEFAULT '1' COMMENT '是否檢查支付密碼:0不需要 1 需要', `check_sms` int(2) NOT NULL DEFAULT '1' COMMENT '是否校驗短信驗證碼:0不需要 1 需要', `create_time` datetime DEFAULT NULL COMMENT '創建時間', `modify_time` datetime DEFAULT NULL COMMENT '修改時間', PRIMARY KEY (`permission_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
角色權限關聯表 sys_permission_role
CREATE TABLE `sys_permission_role` ( `id` int(11) NOT NULL AUTO_INCREMENT, `role_id` int(11) DEFAULT NULL, `permission_id` int(11) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
關于怎么在SpringBoot和Redis中實現一個Token權限認證功能問題的解答就分享到這里了,希望以上內容可以對大家有一定的幫助,如果你還有很多疑惑沒有解開,可以關注億速云行業資訊頻道了解更多相關知識。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。