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信息

alt

我们可以看到,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开发的权限拦截逻辑。

  1. 先通过注解对象获取资源的权限信息preAuthority.roles()。
  2. 通过ThreadLocal机制,获取用户的权限信息UserDetail details = (UserDetail)SecurityContextHolder.get();
  3. 分析用户是否又访问该资源的权限,没有权限则拒绝访问。
@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项目,启动项目,项目结构如下:

alt

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

alt

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

alt

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

alt

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

4. Spring Security分析总结

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

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

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

alt