java 安全架构设计_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 roles = new ArrayList();

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 roles = new ArrayList();

roles.add("user");

userDetail.setRoles(roles);

operator.set(JdkApiInterceptor.USER_REDIS_DETAIL + ":" + username, JSONUtil.toJsonStr(userDetail));

}

redis中保存的session信息

ce0d3c8bfa350e674edc4c17f4c5692e.png

我们可以看到,admin用户拥有所有权限:admin,user。而user用户的权限是:user。

资源权限控制

通过@PreAuthority注解来控制接口的访问权限,如果用户没有访问权限,则拒绝用户;如果有权限,则不拦截并执行相关业务逻辑。

这两个接口 /admin , /user分别要求访问的用户权限是admin和user。admin管理员权限可以访问全部接口,但是user用户则只能访问接口/user,接口/admin则拒绝访问。

那么这种控制如何实现呢?接下来看看AOP的试下原理。

@PostMapping(value = {"/admin"})

@PreAuthority(roles= "admin")

@ResponseBodypublicRestResponseBo admin() {

RestResponseBo result = new RestResponseBo<>(true);

result.setPayload("admin 才能访问的信息");returnresult;

}

@PostMapping(value= {"/user"})

@PreAuthority(roles= "user")

@ResponseBodypublicRestResponseBo user() {

RestResponseBo result = new RestResponseBo<>(true);

result.setPayload("user 访问的信息");returnresult;

}

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 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项目,启动项目,项目结构如下:

d261eff1bff5909667689f4cad777ddc.png

启动成功后,访问localhost:8080,分别使用两个账号登入(admin/admin , user/user)。

b26720017b05de845c60f5b4a8bf5382.png

使用两个账号登入后操作点击按钮访问接口/admin,/user。查看安全控制效果。

45fab7ff9dae3403cc9c8a8af9a4e809.png

我们发现admin用户能访问所有接口,而user用户不能访问/admin接口。提示如下,操作失败:权限不够。

3e1cbf42f02d2e5dda862997bb1accab.png

到此,demo的测试成功,安全服务成功了。我们已经简单实现了security框架的核心功能。

4. Spring Security分析总结

Spring Security框架的核心功能跟demo的实现是一样的,Spring Security的领域模型设计更加完善,鉴权,认证,授权等都有非常完整的领域对象,大家在学习Spring Security的时候,可以对比着demo来学习它的核心机制。笔者就写到这里了,相关Spring Security的学习,大家可以查看我的博客网站的Spring Security系列文章,我将从0到1地为读者朋友们介绍分析。

查看更多 “Java架构师方案” 系列文章 以及 SpringBoot2.0学习示例

介绍

SpringBoot2.0-Jackdking 使用的各种示例,整合流行的中间件,此开源项目中的每个示例都是站在初学者角度,细心剖析技术实现细节,帮助初学者快速掌握 Spring Boot2.0 各组件的使用。

文章

完整的demo项目,请关注公众号“前沿科技bot“并发送"SSS"获取。


版权声明:本文为weixin_39719749原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。