SpringBoot和Redis實(shí)現(xiàn)Token權(quán)限認(rèn)證的實(shí)例講解
一、引言
登陸權(quán)限控制是每個(gè)系統(tǒng)都應(yīng)必備的功能,實(shí)現(xiàn)方法也有好多種。下面使用Token認(rèn)證來(lái)實(shí)現(xiàn)系統(tǒng)的權(quán)限訪問(wèn)。
功能描述:
用戶登錄成功后,后臺(tái)返回一個(gè)token給調(diào)用者,同時(shí)自定義一個(gè)@AuthToken注解,被該注解標(biāo)注的API請(qǐng)求都需要進(jìn)行token效驗(yàn),效驗(yàn)通過(guò)才可以正常訪問(wèn),實(shí)現(xiàn)接口級(jí)的鑒權(quán)控制。
同時(shí)token具有生命周期,在用戶持續(xù)一段時(shí)間不進(jìn)行操作的話,token則會(huì)過(guò)期,用戶一直操作的話,則不會(huì)過(guò)期。
二、環(huán)境
SpringBoot
Redis(Docke中鏡像)
MySQL(Docker中鏡像)
三、流程分析
1、流程分析
(1)、客戶端登錄,輸入用戶名和密碼,后臺(tái)進(jìn)行驗(yàn)證,如果驗(yàn)證失敗則返回登錄失敗的提示。
如果驗(yàn)證成功,則生成 token 然后將 username 和 token 雙向綁定 (可以根據(jù) username 取出 token 也可以根據(jù) token 取出username)存入redis,同時(shí)使用 token+username 作為key把當(dāng)前時(shí)間戳也存入redis。并且給它們都設(shè)置過(guò)期時(shí)間。
(2)、每次請(qǐng)求接口都會(huì)走攔截器,如果該接口標(biāo)注了@AuthToken注解,則要檢查客戶端傳過(guò)來(lái)的Authorization字段,獲取 token。
由于 token 與 username 雙向綁定,可以通過(guò)獲取的 token 來(lái)嘗試從 redis 中獲取 username,如果可以獲取則說(shuō)明 token 正確,反之,說(shuō)明錯(cuò)誤,返回鑒權(quán)失敗。
(3)、token可以根據(jù)用戶使用的情況來(lái)動(dòng)態(tài)的調(diào)整自己過(guò)期時(shí)間。
在生成 token 的同時(shí)也往 redis 里面存入了創(chuàng)建 token 時(shí)的時(shí)間戳,每次請(qǐng)求被攔截器攔截 token 驗(yàn)證成功之后,將當(dāng)前時(shí)間與存在 redis 里面的 token 生成時(shí)刻的時(shí)間戳進(jìn)行比較,當(dāng)當(dāng)前時(shí)間的距離創(chuàng)建時(shí)間快要到達(dá)設(shè)置的redis過(guò)期時(shí)間的話,就重新設(shè)置token過(guò)期時(shí)間,將過(guò)期時(shí)間延長(zhǎng)。
如果用戶在設(shè)置的 redis 過(guò)期時(shí)間的時(shí)間長(zhǎng)度內(nèi)沒(méi)有進(jìn)行任何操作(沒(méi)有發(fā)請(qǐng)求),則token會(huì)在redis中過(guò)期。
四、具體代碼實(shí)現(xiàn)
1、自定義注解
@Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface AuthToken { }
2、登陸控制器
@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); //設(shè)置key生存時(shí)間,當(dāng)key過(guò)期時(shí),它會(huì)被自動(dòng)刪除,時(shí)間是秒 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()); //用完關(guān)閉 jedis.close(); result.put("status", "登錄成功"); result.put("token", token); } else { result.put("status", "登錄失敗"); } return ResponseTemplate.builder() .code(200) .message("登錄成功") .data(result) .build(); } //測(cè)試權(quán)限訪問(wèn) @RequestMapping(value = "test", method = RequestMethod.GET) @AuthToken public ResponseTemplate test() { logger.info("已進(jìn)入test路徑"); return ResponseTemplate.builder() .code(200) .message("Success") .data("test url") .build(); } }
3、攔截器
@Slf4j public class AuthorizationInterceptor implements HandlerInterceptor { //存放鑒權(quán)信息的Header名稱,默認(rèn)是Authorization private String httpHeaderName = "Authorization"; //鑒權(quán)失敗后返回的錯(cuò)誤信息,默認(rèn)為401 unauthorized private String unauthorizedErrorMessage = "401 unauthorized"; //鑒權(quán)失敗后返回的HTTP錯(cuò)誤碼,默認(rèn)為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注解則需要驗(yàn)證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()); } //用完關(guān)閉 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 { } }
4、測(cè)試結(jié)果
五、小結(jié)
登陸權(quán)限控制,實(shí)際上利用的就是攔截器的攔截功能。因?yàn)槊恳淮握?qǐng)求都要通過(guò)攔截器,只有攔截器驗(yàn)證通過(guò)了,才能訪問(wèn)想要的請(qǐng)求路徑,所以在攔截器中做校驗(yàn)Token校驗(yàn)。
想要代碼,可以去GitHub上查看。
https://github.com/Hofanking/token-authentication.git
攔截器介紹,可以參考 這篇文章
補(bǔ)充:springboot+spring security+redis實(shí)現(xiàn)登錄權(quán)限管理
筆者負(fù)責(zé)的電商項(xiàng)目的技術(shù)體系是基于SpringBoot,為了實(shí)現(xiàn)一套后端能夠承載ToB和ToC的業(yè)務(wù),需要完善現(xiàn)有的權(quán)限管理體系。
在查看Shiro和Spring Security對(duì)比后,筆者認(rèn)為Spring Security更加適合本項(xiàng)目使用,可以總結(jié)為以下2點(diǎn):
1、基于攔截器的權(quán)限校驗(yàn)邏輯,可以針對(duì)ToB的業(yè)務(wù)接口來(lái)做相關(guān)的權(quán)限校驗(yàn),以筆者的項(xiàng)目為例,ToB的接口請(qǐng)求路徑以/openshop/api/開頭,可以根據(jù)接口請(qǐng)求路徑配置全局的ToB的攔截器;
2、Spring Security的權(quán)限管理模型更簡(jiǎn)單直觀,對(duì)權(quán)限、角色和用戶做了很好的解耦。
以下介紹本項(xiàng)目的實(shí)現(xiàn)步驟
一、在項(xiàng)目中添加Spring相關(guān)依賴
<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>
二、使用模板模式定義權(quán)限管理攔截器抽象類
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; } //檢查權(quán)限 Collection<? extends GrantedAuthority> authorities = getAttributes(userId); Collection<ConfigAttribute> configAttributes = getAttributes(request); return accessDecisionManager.decide(authorities,configAttributes); } //獲取用戶id public abstract String getUserId(); //根據(jù)用戶id獲取用戶的角色集合 public abstract Collection<? extends GrantedAuthority> getAttributes(String userId); //查詢請(qǐng)求需要的權(quán)限 public abstract Collection<ConfigAttribute> getAttributes(HttpServletRequest request); }
三、權(quán)限管理攔截器實(shí)現(xiàn)類 AuthenticationInterceptor
@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 { } }
四、用戶Session信息管理類
集成redis維護(hù)用戶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); } } }
五、用戶權(quán)限管理service
@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 獲取用戶權(quán)限列表 * @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 獲取當(dāng)前請(qǐng)求所需權(quán)限 * @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; } }
六、權(quán)限校驗(yàn)類 AccessDecisionManager
通過(guò)查看authorities中的權(quán)限列表是否含有configAttributes中所需的權(quán)限,判斷用戶是否具有請(qǐng)求當(dāng)前資源或者執(zhí)行當(dāng)前操作的權(quán)限。
@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; } }
七、配置攔截規(guī)則
@Configuration public class WebAppConfigurer extends WebMvcConfigurerAdapter { @Resource private AbstractAuthenticationInterceptor authenticationInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { // 多個(gè)攔截器組成一個(gè)攔截器鏈 // addPathPatterns 用于添加攔截規(guī)則 // excludePathPatterns 用戶排除攔截 //對(duì)來(lái)自/openshop/api/** 這個(gè)鏈接來(lái)的請(qǐng)求進(jìn)行攔截 registry.addInterceptor(authenticationInterceptor).addPathPatterns("/openshop/api/**"); super.addInterceptors(registry); } }
八 相關(guān)表說(shuō)明
用戶表 sys_user
CREATE TABLE `sys_user` ( `user_id` varchar(64) NOT NULL COMMENT '用戶ID', `username` varchar(255) DEFAULT NULL COMMENT '登錄賬號(hào)', `first_login` datetime(6) NOT NULL COMMENT '首次登錄時(shí)間', `last_login` datetime(6) NOT NULL COMMENT '上次登錄時(shí)間', `pay_pwd` varchar(100) DEFAULT NULL COMMENT '支付密碼', `chant_id` varchar(64) NOT NULL DEFAULT '-1' COMMENT '關(guān)聯(lián)商戶id', `create_time` datetime DEFAULT NULL COMMENT '創(chuàng)建時(shí)間', `modify_time` datetime DEFAULT NULL COMMENT '修改時(shí)間', 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 '創(chuàng)建時(shí)間', `modify_time` datetime DEFAULT NULL COMMENT '修改時(shí)間', PRIMARY KEY (`role_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
用戶角色關(guān)聯(lián)表 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
權(quán)限表 sys_premission
CREATE TABLE `sys_permission` ( `permission_id` int(11) NOT NULL, `name` varchar(255) DEFAULT NULL COMMENT '權(quán)限名稱', `description` varchar(255) DEFAULT NULL COMMENT '權(quán)限描述', `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 '是否校驗(yàn)短信驗(yàn)證碼:0不需要 1 需要', `create_time` datetime DEFAULT NULL COMMENT '創(chuàng)建時(shí)間', `modify_time` datetime DEFAULT NULL COMMENT '修改時(shí)間', PRIMARY KEY (`permission_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
角色權(quán)限關(guān)聯(lián)表 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
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教。
相關(guān)文章
IDEA 2022 CPU占用100%的問(wèn)題及解決方法
這篇文章主要介紹了IDEA 2022 CPU占用100%問(wèn)題及解決方法,其實(shí)解決方法很簡(jiǎn)單,只需要禁用三個(gè)插件然后重啟idea即可成功解決,需要的朋友可以參考下本文2022-08-08詳解@ConfigurationProperties實(shí)現(xiàn)原理與實(shí)戰(zhàn)
這篇文章主要介紹了詳解@ConfigurationProperties實(shí)現(xiàn)原理與實(shí)戰(zhàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-10-10教你利用SpringBoot寫一個(gè)屬于自己的Starter
如果我們將可獨(dú)立于業(yè)務(wù)代碼之外的功配置模塊封裝成一個(gè)個(gè)starter,復(fù)用的時(shí)候只需要將其在pom中引用依賴即可,SpringBoot為我們完成自動(dòng)裝配,簡(jiǎn)直不要太爽,這篇文章主要給大家介紹了關(guān)于如何利用SpringBoot寫一個(gè)屬于自己的Starter,需要的朋友可以參考下2022-03-03一文搞懂并學(xué)會(huì)使用SpringBoot的Actuator運(yùn)行狀態(tài)監(jiān)控組件的詳細(xì)教程
這篇文章主要介紹了一文搞懂并學(xué)會(huì)使用SpringBoot的Actuator運(yùn)行狀態(tài)監(jiān)控組件,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-09-09Java?Stream比較兩個(gè)List的差異并取出不同的對(duì)象四種方法
今天開發(fā)一個(gè)需求時(shí)要對(duì)A和B兩個(gè)List集合遍歷,并比較出集合A有,而集合B沒(méi)有的值,下面這篇文章主要給大家介紹了關(guān)于Java?Stream比較兩個(gè)List的差異并取出不同對(duì)象的四種方法,需要的朋友可以參考下2024-01-01