阅读这篇文章,跟着笔者一起从0到1开始写一个模拟Spring Security框架的工具。 读完文章,你将了解Spring Security核心原理。 本文demo是在Java架构师方案宝典系列中的jackdking-login-redis-token项目基础上衍生出来的。
在导读中,笔者说这篇文章demo是在jackdking-login-redis-token项目基础上开发的,这个项目的具体是如何建立的可查看Java架构师方案宝典系列的这篇文章:Java架构师方案—分布式session基于redis的共享机制(附完整项目代码)。 详细讲解了项目的从0到1的建设过程。
认证 在demo里,笔者没有添加对数据库的访问,而是直接将两种用户admin/admin、user/user放在代码中,通过下面的方式来实现认证逻辑,正常情况下是:先根据用户名来访问数据库,查出用户信息后再比对密码完成认证。
Security的userdetail是要求开发者完整实现访问数据库并完成认证逻辑的。
if(!(username.equals("admin")&&password.equals("admin"))&&!(username.equals("user")&&password.equals("user"))) { return RestResponseBo.fail("用户名或者密码不正确!"); }
分配权限
在demo中,用户的session信息会保存在redis中,用户的cookie中只保存sessionId,每次访问都会根据sessionId来取出session信息。其中权限信息就会保存在sessin信息中。
admin:管理员 user: 普通用户
if(username.equals("admin")) { UserDetail userDetail = new UserDetail(); userDetail.setUsername(username); List<String> roles = new ArrayList<String>(); roles.add("admin"); roles.add("user"); userDetail.setRoles(roles); operator.set(JdkApiInterceptor.USER_REDIS_DETAIL + ":" + username, JSONUtil.toJsonStr(userDetail)); } if(username.equals("user")){ UserDetail userDetail = new UserDetail(); userDetail.setUsername(username); List<String> roles = new ArrayList<String>(); roles.add("user"); userDetail.setRoles(roles); operator.set(JdkApiInterceptor.USER_REDIS_DETAIL + ":" + username, JSONUtil.toJsonStr(userDetail)); }
redis中保存的session信息
我们可以看到,admin用户拥有所有权限:admin,user。而user用户的权限是:user。
资源权限控制
通过@PreAuthority注解来控制接口的访问权限,如果用户没有访问权限,则拒绝用户;如果有权限,则不拦截并执行相关业务逻辑。
这两个接口 /admin , /user分别要求访问的用户权限是admin和user。admin管理员权限可以访问全部接口,但是user用户则只能访问接口/user,接口/admin则拒绝访问。
那么这种控制如何实现呢?接下来看看AOP的试下原理。
@PostMapping(value = {"/admin"}) @PreAuthority(roles= "admin") @ResponseBody public RestResponseBo admin() { RestResponseBo<String> result = new RestResponseBo<>(true); result.setPayload("admin 才能访问的信息"); return result; } @PostMapping(value = {"/user"}) @PreAuthority(roles= "user") @ResponseBody public RestResponseBo user() { RestResponseBo<String> result = new RestResponseBo<>(true); result.setPayload("user 访问的信息"); return result; }
AOP切面控制逻辑
切面编程原理在这里就不再细说,着重讲一下利用aop开发的权限拦截逻辑。
- 先通过注解对象获取资源的权限信息preAuthority.roles()。
- 通过ThreadLocal机制,获取用户的权限信息UserDetail details = (UserDetail)SecurityContextHolder.get();
- 分析用户是否又访问该资源的权限,没有权限则拒绝访问。
@Aspect @Component public class AuthorityAspect { @Around("@annotation(preAuthority)") public Object preAuthority(ProceedingJoinPoint proceedingJoinPoint , PreAuthority preAuthority){ // DataSourceType curType = dbType.value(); String [] authority = preAuthority.roles(); System.out.println("print: "+authority[0]); //判断权限逻辑 if(!ObjectUtils.isEmpty(authority)) { boolean isThrough = false; UserDetail details = (UserDetail)SecurityContextHolder.get(); List<String> roles = details.getRoles(); for(String s : authority) if(roles.contains(s)||roles.contains("admin"))//如果 权限中有一个是 资源权限则通过 ,管理员也通过 isThrough = true; if(!isThrough) return RestResponseBo.fail("权限不够"); } //业务方法 //访问目标方法的参数: Object[] args = proceedingJoinPoint.getArgs(); Object result = null; try { result = proceedingJoinPoint.proceed(); } catch (Throwable e) { // TODO Auto-generated catch block e.printStackTrace(); } return result; } }
用户权限信息传递逻辑
笔者使用了ThreadLocal机制,如果读者不熟悉ThreadLocal值传递机制,大家可以查看我的这篇文章:。
ThreadLocal能跨方法进行值传递,不需要通过方法参数进行传递数据。用户访问的时候,请求线程在springmvc层的HandlerInterceptor中就已经将用户的session信息获取到并放到线程对象中。
第一步,从redis中获取session数据:JSONObject obj = JSONUtil.parseObj(redis.get(USER_REDIS_DETAIL + ":" + userName)); 第二步,将session,数据放入到线程对象中:SecurityContextHolder.set(JSONUtil.toBean(obj, UserDetail.class));
/** * 拦截请求,在controller调用之前 * 返回 false:请求被拦截,返回 * 返回 true :请求OK,可以继续执行,放行 */ @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object arg2) throws Exception { //获取用户cookies String userName = CookieUtil.getCookie("userName"); //放开登入接口 // String uri = request.getRequestURI(); // logger.info("请求uri:" + uri); // // if(uri.equals("loginCheck")) // return true; // logger.info(" ======= 拦截UserId:" + userName); //用户id和token都不为空 if (!StringUtils.isEmpty(userName)) { //根据userid生成唯一key从redis中查出唯一token String uniqueToken = redis.get(USER_REDIS_SESSION + ":" + userName); logger.info("拦截uniqueToken:" + uniqueToken); //如果唯一token为空 ,则拦截url重定向到登入页面 if (StringUtils.isEmpty(uniqueToken)) { response.sendRedirect("/login"); returnErrorResponse(response, "请登录..."); return false; } //用户id和token有一个为空,则重定向登入页面 } else { response.sendRedirect("/login"); returnErrorResponse(response,"请登录..."); return false; } //从redis服务器中获取用户session信息,包括权限信息。 JSONObject obj = JSONUtil.parseObj(redis.get(USER_REDIS_DETAIL + ":" + userName)); SecurityContextHolder.set(JSONUtil.toBean(obj, UserDetail.class)); return true; }
导入jackdking-login-security-simulator项目,启动项目,项目结构如下:
启动成功后,访问localhost:8080,分别使用两个账号登入(admin/admin , user/user)。
使用两个账号登入后操作点击按钮访问接口/admin,/user。查看安全控制效果。
我们发现admin用户能访问所有接口,而user用户不能访问/admin接口。提示如下,操作失败:权限不够。
到此,demo的测试成功,安全服务成功了。我们已经简单实现了security框架的核心功能。
Spring Security框架的核心功能跟demo的实现是一样的,Spring Security的领域模型设计更加完善,鉴权,认证,授权等都有非常完整的领域对象,大家在学习Spring Security的时候,可以对比着demo来学习它的核心机制。笔者就写到这里了,相关Spring Security的学习,大家可以查看我的博客网站的Spring Security系列文章,我将从0到1地为读者朋友们介绍分析。
查看更多 “Java架构师方案” 系列文章 以及 SpringBoot2.0学习示例
完整的demo项目,请关注公众号“前沿科技bot“并发送"SSS"获取。