一、引言
登陸權限控制是每個系統都應必備的功能,實現方法也有好多種。下面使用Token認證來實現系統的權限訪問。
功能描述:
用戶登錄成功后,后臺返回一個token給調用者,同時自定義一個@AuthToken注解,被該注解標注的API請求都需要進行token效驗,效驗通過才可以正常訪問,實現接口級的鑒權控制。
同時token具有生命周期,在用戶持續一段時間不進行操作的話,token則會過期,用戶一直操作的話,則不會過期。
二、環境
SpringBoot
Redis(Docke中鏡像)
MySQL(Docker中鏡像)
三、流程分析
1、流程分析
(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中過期。
四、具體代碼實現
1、自定義注解
1
2
3
4
|
@Target ({ElementType.METHOD, ElementType.TYPE}) @Retention (RetentionPolicy.RUNTIME) public @interface AuthToken { } |
2、登陸控制器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
|
@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(); } } |
3、攔截器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
|
@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 { } } |
4、測試結果
五、小結
登陸權限控制,實際上利用的就是攔截器的攔截功能。因為每一次請求都要通過攔截器,只有攔截器驗證通過了,才能訪問想要的請求路徑,所以在攔截器中做校驗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的權限管理模型更簡單直觀,對權限、角色和用戶做了很好的解耦。
以下介紹本項目的實現步驟
一、在項目中添加Spring相關依賴
1
2
3
4
5
6
7
8
9
10
|
< 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 > |
二、使用模板模式定義權限管理攔截器抽象類
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
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); } |
三、權限管理攔截器實現類 AuthenticationInterceptor
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
@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維護用戶session信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
|
@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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
|
@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 ; } } |
六、權限校驗類 AccessDecisionManager
通過查看authorities中的權限列表是否含有configAttributes中所需的權限,判斷用戶是否具有請求當前資源或者執行當前操作的權限。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
@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 ; } } |
七、配置攔截規則
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
@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
1
2
3
4
5
6
7
8
9
10
11
|
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
1
2
3
4
5
6
7
|
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
1
2
3
4
5
6
|
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
1
2
3
4
5
6
7
8
9
10
11
|
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
1
2
3
4
5
6
|
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 |
以上為個人經驗,希望能給大家一個參考,也希望大家多多支持服務器之家。如有錯誤或未考慮完全的地方,望不吝賜教。
原文鏈接:https://blog.csdn.net/zxd1435513775/article/details/86555130