一、引言
登陆权限控制是每个系统都应必备的功能,实现方法也有好多种。下面使用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、自定义注解
@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); //设置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、拦截器
@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相关依赖
<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); }
三、权限管理拦截器实现类 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维护用户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
@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中所需的权限,判断用户是否具有请求当前资源或者执行当前操作的权限。
@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
以上为个人经验,希望能给大家一个参考,也希望大家多多支持易盾网络。如有错误或未考虑完全的地方,望不吝赐教。