Java架构师方案——模拟Spring Security,我徒手写了一个简单的安全框架(附完整项目代码)
1. 导读
阅读这篇文章,跟着笔者一起从0到1开始写一个模拟Spring Security框架的工具。
读完文章,你将了解Spring Security核心原理。
本文demo是在Java架构师方案宝典系列中的jackdking-login-redis-token项目基础上衍生出来的。
2. 核心的组件和逻辑
在导读中,笔者说这篇文章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;
}
3. 运行测试
导入jackdking-login-security-simulator项目,启动项目,项目结构如下:
启动成功后,访问localhost:8080,分别使用两个账号登入(admin/admin , user/user)。
使用两个账号登入后操作点击按钮访问接口/admin,/user。查看安全控制效果。
我们发现admin用户能访问所有接口,而user用户不能访问/admin接口。提示如下,操作失败:权限不够。
到此,demo的测试成功,安全服务成功了。我们已经简单实现了security框架的核心功能。
4. Spring Security分析总结
Spring Security框架的核心功能跟demo的实现是一样的,Spring Security的领域模型设计更加完善,鉴权,认证,授权等都有非常完整的领域对象,大家在学习Spring Security的时候,可以对比着demo来学习它的核心机制。笔者就写到这里了,相关Spring Security的学习,大家可以查看我的博客网站的Spring Security系列文章,我将从0到1地为读者朋友们介绍分析。
完整的demo项目,请关注公众号“前沿科技bot“并发送"SSS"获取。
- 本文标签: Spring Boot Spring Security Java架构师方案
- 版权声明: 本站原创文章,于2020年10月31日由空白发布,转载请注明出处