这是一个Java的安全认证框架,实现登录,权限认证功能。脱离容器的会话ID,轻松实现负载均衡而不用担心会话丢失。支持多种会话缓存实现,权限控制灵活,与spring框架配置使用简单。

获取用户登录信息

SessionManager sm = ...;
Session session = sm.getSession();
if(session.isLogin()) {
String userId = session.getUserId();
}

sm.getSession()可以获取到用户的当前会话,从会话中你可以获取访问凭证,用户ID等信息,该方法不会返回Null(我们下面会说到如何获取到SessionManager)。

用户登录

@Autowire
private SessionManager sm;
@PostMapping("login")
String login(String username, String password) {
UsernamePasswordAuthentication auth = new UsernamePasswordAuthentication(username, password);
SessionWrapper wrapper = sm.login(auth);
Session session = wrapper.getSession();
Map data = wrapper.getExtras();//我们自己添加的额外信息
String token = session.getAccessToken().toString();
return token;
}

我们使用框架自带的UsernamePasswordAuthentication认证类来传递账号和密码进行登录,如果你希望传递更多的信息到“登录处理器”中,则需要继承该类或实现Authentication接口,比如你需要区别后台用户和会员的用户类型。

登录成功后,我们得到一个包装类,你可以获取到访问凭证token,然后将这个token返回给客户端,客户端请求“受保护资源”的时候,需要带上这个token,否则会视为无权限。

配置

在使用框架前,我们需要做一些配置,比如处理登录操作的服务类,权限数据源,登录信息缓存等。下面我们来一个个看怎么配置这些东西,以下的代码我们是基于Spring Framework框架的。

登录处理器

创建一个类实现LoginHandler接口,该接口只有一个登录方法。

public class LoginService implements LoginHandler {
@Override
public LoginResult login(Authentication auth) {
//转成什么类,取决你使用SessionManager#login时传递什么东西
UsernamePasswordAuthentication up = (UsernamePasswordAuthentication)auth;
String username = up.getUsername();
String password = up.getPassword();
//查询数据库,判断用户是否存在
//...
String userId = ...;
Set resourceId = ...;
LoginResult result = new LoginResult(userId, resourceId);
return result;
}
}

处理器返回一个LoginResult对象,你需要将用户的ID和用户拥有的权限(资源)ID放到LoginResult中。

缓存

缓存是用来存储用户登录后的信息,比如用户ID,用户拥有的权限信息,其他扩展信息等,因为每次请求我们都需要去检索这些信息来做验证,所以这些信息应该放在一个可快速读取到的地方,如内存,Redis等。我们内置了一些缓存实现,你可以直接拿来用。

FileSessionCache

一般用于开发阶段,在开发时,我们经常需要重启服务,登录信息保存在文件中的话,重启后也不会导致会话消失。

MapSessionCache

基于HashMap实现的内存型缓存,适用于小项目,用户基础不大的项目。

JedisSessionCache

基于Redis实现的。

SpringRedisSessionCache

也是基于Redis实现,与JedisSessionCache的区别是,JedisSessionCache需要你自己提供Redis的实现(比如JedisPool),而SpringRedisSessionCache使用你项目中已经定义的缓存实现(如果你项目有其他地方也用到缓存)。

如果上面的缓存无法满足你的需求,你可以自己实现,只需要实现SessionCache接口即可,下面我们就来看看怎么定义缓存。首先创建一个spring的配置类:

@Configuration
public class SecurityConfig {
}
在里面定义我们的Bean。
//此处定义是在SecurityConfig里面的
@Bean
public SessionCache sessionCache() {
return new MapSessionCache();
}
SessionManager

SessionManager是一个使用最频繁的类,当配置都设置好后,在应用程序中你需要获取用户会话信息时,都需要从该类中获取。

@Bean
public SessionManager sessionManager(LoginHandler loginHandler, SessionCache cache) {
//loginHandler和cache我们前面已经定义了
SessionManager manager = new SessionManager(loginHandler, cache);
//我们返回给客户端的访问凭证是使用用户ID等信息加密生成的,你需要提供一个密钥
manager.setAccessTokenSecretKey("my-secret-key");
return manager;
}
//为SessionManager绑定请求事件,主要用户访问凭证的解析,以及在其他地方能够获取到ServletRequest对象
@Bean
public ServletListenerRegistrationBean registerRequestHodler(SessionManager sessionManager){
ServletListenerRegistrationBean bean = new ServletListenerRegistrationBean();
bean.setListener(sessionManager);
return bean;
}

权限

也就是保护资源,比较常见的保护资源有URL,我们需要定义一个数据源来告诉验证器哪些资源(URL)是受保护的。

@Bean
public UriResourceDataSource uriResourceDataSource() {
//我们定义了两个URL资源,如果用户访问这些URL时,没有对应的权限ID,就会被拒绝。
//URL匹配规则请参考spring的AntPathMatcher
UriResourceDataSource ds = new UriResourceDataSource();
ds.addResource(SimpleUriResource.of("001", "/admin/**"));
ds.addResource(SimpleUriResource.of("002", "/member"));
return ds;
}

验证器

我们上面定义了很多对象,目的是为了在验证器中使用这些对象来做拦截验证,通常验证的规则如下:

拦截Http请求。

获取URL信息,然后从“数据源”中查找一个URL资源,如果找到,表示这个请求是受保护的资源。

获取用户会话。

将保护资源和会话交给验证程序。

我们提供了一个验证工具HttpSecurityService,通常你需要根据你的应用环境自己写一个拦截器做验证,目前我们只提供spring框架的拦截器,如果你是用spring框架,则可以直接使用我们的验证器。

@Bean
public SecurityValidationInterceptor securityInterceptor() {
SecurityValidationInterceptor interceptor =
new SecurityValidationInterceptor();
return interceptor;
}

验证失败的处理

默认的,验证失败后会返回401状态码,如果你希望自己处理验证失败的结果,实现SecurityValidationListener接口。

public class SecurityFailListener implements SecurityValidationListener {
@Override
public void onFail(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, HttpSecurityService.ValidationResult r) {
//在这里处理验证错误结果,你可以从ValidationResult中获取失败的原因
//然后使用httpServletResponse返回消息
}
}
//然后加到SecurityValidationInterceptor中
interceptor.setOnSecurityValidationFail(new SecurityFailListener());

至此,验证框架所需的东西都已经准备就绪。

使用访问凭证发送请求

用户登录成功后,你可以得到访问凭证session.getAccessToken().toString(),并返回给客户端,客户端每次发送请求都带上这个凭证。

http://domain/path?_token=your_token

或者你可以放在请求头中:

X-User-Token: your_token

如果你希望修改凭证参数名称或请求头名称,可以通过AccessTokenReader类修改。

AccessTokenReader.setTokenQueryParamName("token_name");

AccessTokenReader.setTokenHeaderName("token_head_name");

进阶

过期时间设置

会话过期时间)

会话是存储在缓存中的,每次访问时,会话的过期时间都会被更新,你可以使用SessionManager#setSessionTimeout来设置会话的过期时间,默认为两个小时。

凭证过期时间)

凭证的过期时间被加密在凭证自身里面,凭证的过期时间是不会更新的,这是为了防止凭证被盗用后,可以一直无限使用,当凭证过期了后,那么就会被丢弃。凭证的过期时间默认为24小时,可以使用SessionManager#setAccessTokenTimeout修改。

会话过期和凭证过期的区别就是,会话过期能够被更新,凭证不能,当凭证过期后,会话自然无法再访问,比如凭证过期为24小时,会话为1分钟,如果用户每隔30秒发送请求,那么会话能被使用24小时。如果凭证过期为1小时,会话过期为2小时,那么1小时后会话也会无法访问。

将额外的信息混合到凭证中

默认,凭证信息由一个UUID和过期时间生成,如果你希望加入一些额外信息进去参加生成,可以这么做:

//在登录处理器中
LoginResult result = ...;
result.setTokenMergeInfo(user::getName);
return result;
setTokenMergeInfo方法接收一个TokenMergeInfo接口。当你希望获取这个扩展信息时,可以通过Session获取。
Session s = sessionManager.getSession();
String extra = s.getAccessToken().getExtra();

这个扩展信息是不在缓存中的,而是在凭证字符串中的。

修改访问凭证规则

有时候需要为不同的用户生成不同的访问凭证,比如后台管理员和前台注册会员使用不同的凭证串。

@Bean
public SessionManager sessionManager(LoginHandler h, SessionCache c) {
SessionManager manager = new SessionManager(h, c);
manager.setAccessTokenSecretKey("your_secret_key");
// 根据前台和后台的用户生成不同前缀的token
// SecurityAuthentication是自定义类,继承UsernamePasswordAuthentication
manager.setOnAccessTokenBuildListener((r, a, token) -> {
int from = ((SecurityAuthentication)a).getFrom();
String prefix;
if(from == SecurityAuthentication.FROM_FRONTEND) {
prefix = USER_TOKEN_PREFIX_FRONT;
} else {
prefix = USER_TOKEN_PREFIX_BACKEND;
}
return prefix + ":" + token;
});
//当准备解析凭证的时候,你需要还原你修改前的凭证(也就是去掉prefix)
manager.setOnAccessTokenResolveListener(t -> {
String[] tmp = t.split(":");
if(tmp.length == 2) {
RequestContextHolder.getRequestAttributes().setAttribute("user_type", tmp[0], SCOPE_REQUEST);
return tmp[1];
} else {
return t;
}
});
return manager;
}

验证器注解

如果你是使用SecurityValidationInterceptor这个验证器,那么你可以为你的Controller方法加上这两个注解。

@PostMapping("login")
@Anonymous//标识这个方法是非保护资源,不用登录也可以访问
void login() {
}
@GetMapping("user/info")
@OnlyLogin//标识这个方法为只要登录即可方法,不用拥有对应的权限
String getUserName() {
}

这两个注解可以放在方法上,也可以放在类上,放在类上的话表示这个类的所有方法都有效,只有方法被标注了XXXMapping才有效果。