本文主要展示内容有二:
- SpringBoot项目如何整合Shiro框架,从前端请求用户登录,到后端判断用户、角色、权限认证,再到登录后请求获取用户信息,最后用户注销。
- 基于以上整合代码的示例,针对其中shiro框架核心逻辑进行源码解析。
在开始阅读文章之前,建议了解Shiro框架的基本使用,才能更好了解本文中所展示的代码示例,可以阅读此文入门:Shiro框架基本使用。
首先了解下数据库的表结构
这是一个典型的 用户——角色——权限 表结构。用户可以拥有多个角色,角色也可拥有多个权限。
SpringBoot项目导入Shiro依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.2</version>
</dependency>
自定义登录认证Realm类
public class CertificationRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
/**
* 获取用户授权信息
*
* @param principals 封装了用户名的对象
* @return 用户授权信息
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); // 创建用户授权信息对象
User user = (User) principals.getPrimaryPrincipal(); // 获取用户名
List<Role> roles = userService.getRolesByAccount(user.getAccount()); // 根据用户名获取对应的角色列表
Set<String> roleIds = new HashSet<>();
roles.forEach(role -> roleIds.add(role.getId())); // 获取角色列表的id,并封装成set集合
authorizationInfo.setRoles(roleIds); // 添加角色id列表
List<Permission> permissions = userService.getPermissionsByAccount(user.getAccount()); // 根据用户名获取对应的权限列表
Set<String> permissionIds = new HashSet<>();
permissions.forEach(permission -> permissionIds.add(permission.getId())); // 获取权限列表的id,并封装成set集合
authorizationInfo.setStringPermissions(permissionIds); // 添加权限id列表
return authorizationInfo; // 返回用户授权信息对象
}
/**
* 获取用户信息
*
* @param token 用户名、密码token
* @return 用户信息
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String account = (String) token.getPrincipal(); // 获取用户名
User user = userService.getByAccount(account); // 根据用户名获取用户
if (user == null) { // 用户为null,返回null
return null;
}
SimpleAuthenticationInfo authenticationInfo =
new SimpleAuthenticationInfo(user, user.getPassword(), getName()); // 新建用户身份认证对象,并封装用户、用户密码、realm信息
authenticationInfo.setCredentialsSalt(ByteSource.Util.bytes("slot")); // 设置密码加密盐值
return authenticationInfo; // 返回用户身份认证对象
}
}
自定义过滤器,重写Shiro框架默认的过滤器认证失败逻辑
在Shiro框架默认的过滤器实现中,如果用户未登录系统就访问请求,那么在请求到达框架默认的过滤器时,会认证失败,然后重定向到 login.jsp 页面。
这是Shiro框架默认的过滤器实现,在如今前后端项目分离的时代,前后端都是以JSON数据进行交互,因此需要我们自己自定义一个新的过滤器类,重写Shiro框架默认的过滤器认证失败逻辑。
public class LoginVerificationFilter extends FormAuthenticationFilter {
/**
* 自定义过滤器覆盖Shiro框架默认过滤认证失败逻辑
*
* @param request 请求
* @param response 响应
* @return false
* @throws IOException
*/
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException {
response.setCharacterEncoding("UTF-8");
PrintWriter writer = response.getWriter();
writer.write("用户未登录");
writer.flush();
writer.close();
return false;
}
}
创建Shiro配置类,定义相关bean
@Configuration
public class ShiroConfig {
/**
* 创建密码匹配器,定义加密/解密规则
*
* @return 密码匹配器
*/
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("MD5"); // 使用MD5算法加密
hashedCredentialsMatcher.setHashIterations(2); // 加密次数为2
return hashedCredentialsMatcher;
}
/**
* 创建自定义用户认证realm对象,并设置密码匹配器
*
* @return 自定义用户认证realm对象
*/
@Bean
public CertificationRealm certificationRealm(HashedCredentialsMatcher hashedCredentialsMatcher) {
CertificationRealm certificationRealm = new CertificationRealm();
certificationRealm.setCredentialsMatcher(hashedCredentialsMatcher); // 设置密码匹配器
return certificationRealm;
}
/**
* 声明安全管理器
*
* @param certificationRealm 自定义的用户认证realm对象
* @return 安全管理器
*/
@Bean
public DefaultWebSecurityManager defaultWebSecurityManager(CertificationRealm certificationRealm) {
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
defaultWebSecurityManager.setRealm(certificationRealm); // 设置自定义realm对象
return defaultWebSecurityManager;
}
/**
* 设置安全管理器
* 使用自定义的登录校验过滤器代替Shiro框架默认校验过滤器,并指定需要排除校验的路径
*
* @param defaultWebSecurityManager 安全管理器
* @return Shiro过滤器工厂bean
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager); // 设置安全管理器
Map<String, Filter> filterMap = new LinkedHashMap<>();
filterMap.put("authc", new LoginVerificationFilter());
shiroFilterFactoryBean.setFilters(filterMap); // 将自定义的登录校验过滤器,替换掉框架默认的校验过滤器
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
filterChainDefinitionMap.put("/user/login", "anon"); // 校验排除/user/login路径的请求
filterChainDefinitionMap.put("/**", "authc"); // 除了排除的请求,其他请求都需要进行校验
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); // 设置各个定义好路径的过滤器
return shiroFilterFactoryBean;
}
}
此处补充下有关Shiro框架过滤器的实例命名:
- anon:不认证也可以访问,即该过滤器实例中设置的请求路径,不需要进行用户认证即可访问。
- authc:必须认证后才可访问,即该过滤器实例中设置的请求路径,需要进行用户认证,并认证成功才可访问。
最后,创建Controller类进行接收请求
@RestController
@RequestMapping("/user")
public class UserController {
@PostMapping("/login")
public String login(@RequestBody User user) {
String account = user.getAccount();
String password = user.getPassword();
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(account, password);
Subject subject = SecurityUtils.getSubject();
// 校验用户名、密码
try {
subject.login(usernamePasswordToken);
} catch (UnknownAccountException e) {
return "用户不存在";
} catch (IncorrectCredentialsException e) {
return "密码错误";
} catch (Exception e) {
return "登录失败";
}
// 校验角色
if (!subject.hasRole("ar")) {
return "用户无ar角色";
}
// 校验权限
if (!subject.isPermitted("ap")) {
return "用户无ap权限";
}
return "登录成功";
}
@GetMapping("/getInfo")
public User getInfo() {
User user = (User) SecurityUtils.getSubject().getPrincipal();
return user;
}
@PostMapping("/logout")
public String logout() {
SecurityUtils.getSubject().logout();
return "注销成功";
}
}
代码整合完成,打开PostMan发送请求测试功能
首先访问/user/getInfo接口,获取用户信息:
响应结果为“用户未登录”,这里对应的就是之前自定义的LoginVerificationFilter过滤器类中的认证失败方法。
接下来,对用户进行登录,携带参数访问/user/login接口:
用户登录成功(对于其他认证失败的情况,例如:用户不存在,密码错误等情况,大家自行测试)。
然后再次访问/user/getInfo接口,结果如下:
响应结果不再是“用户未登录”,而是返回已登录过的用户信息。
最后,用户进行注销,访问接口/user/logout:
到此,SpringBoot整合Shiro框架完成。如果只是要求掌握SpringBoot项目中如何使用Shiro,那么可以结束阅读了。因为接来下的内容就是针对以上整合的代码,对Shiro框架中的核心源码进行解析。
Shiro核心源码解析
在阅读源码前,先来了解下Shiro框架中核心的组件Security Mananger的继承体系:
在项目中的Security Manager组件使用的正是DefaultWebSecurity Manager(Web安全管理器),而它同时继承了AuthorizingSecurity Manager(授权认证安全管理器)和AuthenticatingSecurity Manager(身份认证安全管理器)。在之后的源码解析中的用户认证流程中会使用到这两个安全管理器,在此先做个简单的了解。
核心组件初始化
首先从Shiro核心组件初始化开始了解源码,因此来看下ShiroConfig配置类中初始化了哪些bean。
初始化核心组件DefaultWebSecurity Mananger:
/**
* 声明安全管理器
*
* @param certificationRealm 自定义的用户认证realm对象
* @return 安全管理器
*/
@Bean
public DefaultWebSecurityManager defaultWebSecurityManager(CertificationRealm certificationRealm) {
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
defaultWebSecurityManager.setRealm(certificationRealm); // 设置自定义realm对象
return defaultWebSecurityManager;
}
进入DefaultWebSecurityManager的构造方法来看下其中的逻辑:
public DefaultWebSecurityManager() {
super(); // 初始化父类
..
}
可以看到在new DefaultWebSecurityManager()方法中,调用了super()对父类的对象也进行了初始化。跟踪调试发现,在上层的构造方法中也会层层调用super(),结合之前的继承体系图,发现所谓的AuthorizingSecurity Manager(授权认证安全管理器)和AuthenticatingSecurity Manager(身份认证安全管理器)也在这时被新建了:
public AuthorizingSecurityManager() {
super();
this.authorizer = new ModularRealmAuthorizer(); // authorizer主要负责存储realm域对象,以及调用授权认证相关方法
}
public AuthenticatingSecurityManager() {
super();
this.authenticator = new ModularRealmAuthenticator(); // authenticator主要负责存储realm域,以及调用身份认证相关方法
}
安全管理器主要是处理与安全相关的认证交互,简单来说,就是身份认证、授权认证方法的调用者。那么认证的途径有了,还需要认证的数据(用户、角色、权限信息),这时候就需要设置对应的Realm:
public void setRealm(Realm realm) { // 此处传进来的是自定义的CertificationRealm对象
if (realm == null) {
throw new IllegalArgumentException("Realm argument cannot be null");
}
Collection<Realm> realms = new ArrayList<Realm>(1); // 定义realm域集合
realms.add(realm); // 添加realm
setRealms(realms); // 设置realm集合
}
public void setRealms(Collection<Realm> realms) {
if (realms == null) {
throw new IllegalArgumentException("Realms collection argument cannot be null.");
}
if (realms.isEmpty()) {
throw new IllegalArgumentException("Realms collection argument cannot be empty.");
}
this.realms = realms; // 属性绑定realm集合
afterRealmsSet();
}
此处需要关注重点方法afterRealmsSet(),跟踪调试后发现不单单是对DefaultWebSecurity Manager设置了realm域,同时也对authorizer和authenticator设置了realm域:
protected void afterRealmsSet() {
super.afterRealmsSet();
if (this.authorizer instanceof ModularRealmAuthorizer) {
((ModularRealmAuthorizer) this.authorizer).setRealms(getRealms()); // 对权限认证管理器设置realm
}
}
protected void afterRealmsSet() {
super.afterRealmsSet();
if (this.authenticator instanceof ModularRealmAuthenticator) {
((ModularRealmAuthenticator) this.authenticator).setRealms(getRealms()); // 对身份认证管理器设置realm
}
}
接着还需要初始化过滤器。因为Shiro的认证功能的实现,同时依赖于它的过滤器过滤非法(例如:未登录状态)用户,所以再来看下过滤器是如何初始化的:
/**
* 设置安全管理器
* 使用自定义的登录校验过滤器代替Shiro框架默认校验过滤器,并指定需要排除校验的路径
*
* @param defaultWebSecurityManager 安全管理器
* @return Shiro过滤器工厂bean
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager); // 设置安全管理器
Map<String, Filter> filterMap = new LinkedHashMap<>();
filterMap.put("authc", new LoginVerificationFilter());
shiroFilterFactoryBean.setFilters(filterMap); // 将自定义的登录校验过滤器,替换掉框架默认的校验过滤器
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
filterChainDefinitionMap.put("/user/login", "anon"); // 校验排除/user/login路径的请求
filterChainDefinitionMap.put("/**", "authc"); // 除了排除的请求,其他请求都需要进行校验
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); // 设置各个定义好路径的过滤器
return shiroFilterFactoryBean;
}
从以上代码可知,Shiro的过滤器主要由ShiroFilterFactoryBean工厂类提供实例,因此看下ShiroFilterFactoryBean的构造方法:
public ShiroFilterFactoryBean() {
this.filters = new LinkedHashMap<String, Filter>();
this.filterChainDefinitionMap = new LinkedHashMap<String, String>(); //order matters!
}
ShiroFilterFactoryBean的构造方法非常简单,只是定义了各个过滤器实例映射,和各个过滤器对应路径映射属性,主要是为了满足开发者定制化的需求(例如,以上例子就体现了替换默认过滤器,定义各过滤器对应过滤的请求路径)。除此之外,同时还注入并设置了已创建好的DefaultWebSecurityManager实例,意味着已规定好用户的认证流程。
用户登录认证
初始化已完成,下面解析用户认证源码,首先来看用户登录源码:
@PostMapping("/login")
public String login(@RequestBody User user) {
... // 省略多余代码
// 校验用户名、密码
try {
subject.login(usernamePasswordToken);
} catch (UnknownAccountException e) {
return "用户不存在";
} catch (IncorrectCredentialsException e) {
return "密码错误";
} catch (Exception e) {
return "登录失败";
}
...
}
public void login(AuthenticationToken token) throws AuthenticationException {
clearRunAsIdentitiesInternal(); // 清除之前的用户登录信息
Subject subject = securityManager.login(this, token); // 使用安全管理器进行登录认证
PrincipalCollection principals;
String host = null;
if (subject instanceof DelegatingSubject) {
DelegatingSubject delegating = (DelegatingSubject) subject;
//we have to do this in case there are assumed identities - we don't want to lose the 'real' principals:
principals = delegating.principals;
host = delegating.host;
} else {
principals = subject.getPrincipals();
}
if (principals == null || principals.isEmpty()) {
String msg = "Principals returned from securityManager.login( token ) returned a null or " +
"empty value. This value must be non null and populated with one or more elements.";
throw new IllegalStateException(msg);
}
this.principals = principals;
this.authenticated = true;
if (token instanceof HostAuthenticationToken) {
host = ((HostAuthenticationToken) token).getHost();
}
if (host != null) {
this.host = host;
}
Session session = subject.getSession(false);
if (session != null) {
this.session = decorate(session);
} else {
this.session = null;
}
}
在每次登录认证前,都需要清除之前登录认证成功所保存到session的信息:
private void clearRunAsIdentitiesInternal() {
try {
clearRunAsIdentities();
} catch (SessionException se) {
log.debug("Encountered session exception trying to clear 'runAs' identities during logout. This can generally safely be ignored.", se);
}
}
private void clearRunAsIdentities() {
Session session = getSession(false); // 获取已存在的session
if (session != null) { // 若不为空,则删除之前的已登录信息
session.removeAttribute(RUN_AS_PRINCIPALS_SESSION_KEY);
}
}
清除之前的登录信息,接着就是进行本次的登录认证:
public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {
AuthenticationInfo info;
try {
info = authenticate(token); // 根据传入的用户token进行登录认证
} catch (AuthenticationException ae) {
...
}
Subject loggedIn = createSubject(token, info, subject);
onSuccessfulLogin(token, info, loggedIn);
return loggedIn;
}
public AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
return this.authenticator.authenticate(token);
}
从这个authenticate的方法体中可以看到,其实真正进行身份认证的操作是由之前说到的this.authenticator去执行:
public final AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
...
AuthenticationInfo info;
try {
info = doAuthenticate(token); // 根据token执行身份认证,并返回用户认证信息
if (info == null) {
String msg = "No account information found for authentication token [" + token + "] by this Authenticator instance. Please check that it is configured correctly.";
throw new AuthenticationException(msg);
}
} catch (Throwable t) {
...
}
...
return info;
}
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
assertRealmsConfigured(); // 断言设置的realm域是否存在
Collection<Realm> realms = getRealms(); // 获取realm域集合
if (realms.size() == 1) { // realm域集合中只存在一个realm域元素
return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken); // 执行单个realm域认证
} else {
return doMultiRealmAuthentication(realms, authenticationToken); // 否则,执行多个realm域认证
}
}
认证前需要对realm域集合进行判断,若realm域集合为空,则说明无法和token中的用户信息作对比认证,直接抛异常即可:
protected void assertRealmsConfigured() throws IllegalStateException {
Collection<Realm> realms = getRealms();
if (CollectionUtils.isEmpty(realms)) {
String msg = "Configuration error: No realms have been configured! One or more realms must be present to execute an authentication attempt.";
throw new IllegalStateException(msg);
}
}
确保存在至少一个realm域,则可以进行登录认证(之前只设置了一个realm域,因此对执行单个realm域认证方法进行解析。多个realm认证也是相同的逻辑,有兴趣可以自行研究):
protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken token) {
...
AuthenticationInfo info = realm.getAuthenticationInfo(token); // 调用realm域对象方法,根据token认证用户信息,并返回用户认证信息
if (info == null) { // 若返回的用户认证信息为空,则说明不存在该用户,抛出UnknownAccountException异常
String msg = "Realm [" + realm + "] was unable to find account data for the submitted AuthenticationToken [" + token + "].";
throw new UnknownAccountException(msg);
}
return info; // 用户存在,返回用户认证信息
}
public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
AuthenticationInfo info = getCachedAuthenticationInfo(token); // 先尝试从缓存中获取用户认证信息
if (info == null) { // 缓存中无用户认证信息
info = doGetAuthenticationInfo(token); // 调用realm对象方法,获取用户认证信息
log.debug("Looked up AuthenticationInfo [{}] from doGetAuthenticationInfo", info);
if (token != null && info != null) { // 获取到的用户认证信息,以及传入token不为空
cacheAuthenticationInfoIfPossible(token, info); // 则尝试加入缓存
}
} else {
log.debug("Using cached authentication info [{}] to perform credentials matching.", info);
}
if (info != null) { // 若用户认证信息不为空
assertCredentialsMatch(token, info); // 则判断是否存在加密匹配器,存在则利用加密匹配器中的加密规则,对token和info进行密码匹配
} else {
log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}]. Returning null.", token);
}
return info; // 最后返回用户认证信息
}
看到doGetAuthenticationInfo方法是否很眼熟呢?是的,此处就是调用之前自定义的CertificationRealm类中获取用户认证信息的方法:
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String account = (String) token.getPrincipal(); // 获取用户名
User user = userService.getByAccount(account); // 根据用户名获取用户
if (user == null) { // 用户为null,返回null
return null;
}
SimpleAuthenticationInfo authenticationInfo =
new SimpleAuthenticationInfo(user, user.getPassword(), getName()); // 新建用户身份认证对象,并封装用户、用户密码、realm信息
authenticationInfo.setCredentialsSalt(ByteSource.Util.bytes("slot")); // 设置密码加密盐值
return authenticationInfo; // 返回用户身份认证对象
}
从doGetAuthenticationInfo方法中可以看到,最后返回的authenticationInfo对象当中封装了用户信息对象、用户密码,以及当前执行认证方法的reaml名:
public SimpleAuthenticationInfo(Object principal, Object credentials, String realmName) {
this.principals = new SimplePrincipalCollection(principal, realmName); // 将传入的用户信息和realm名进行封装,并赋值到principals属性
this.credentials = credentials; // 将传入的用户密码赋值到credentials属性
}
以上代码还有个细节,就是将用户信息和realm名进行统一的封装成SimplePrincipalCollection对象,这里看下SimplePrincipalCollection的构造方法:
public SimplePrincipalCollection(Object principal, String realmName) {
// 添加用户信息和realm名
if (principal instanceof Collection) {
addAll((Collection) principal, realmName);
} else {
add(principal, realmName);
}
}
添加用户信息:
public void add(Object principal, String realmName) {
if (realmName == null) {
throw new IllegalArgumentException("realmName argument cannot be null.");
}
if (principal == null) {
throw new IllegalArgumentException("principal argument cannot be null.");
}
this.cachedToString = null;
getPrincipalsLazy(realmName).add(principal); // 根据realm名初始化用于存储用户信息的集合,再添加用户信息
}
根据传入的realm名初始化用户信息集合(实际上就是新建Map,以realm名为键,以用户信息Set集合为value保存对应的用户信息):
protected Collection getPrincipalsLazy(String realmName) {
if (realmPrincipals == null) {
realmPrincipals = new LinkedHashMap<String, Set>(); // 新建Map
}
Set principals = realmPrincipals.get(realmName); // 先尝试根据realm名获取对应的用户信息集合
if (principals == null) { // 用户信息集合为空,则添加新的集合
principals = new LinkedHashSet();
realmPrincipals.put(realmName, principals);
}
return principals; // 返回对应的用户信息集合
}
返回Set集合后,再将传入的用户信息对象添加进去。到此,返回的SimpleAuthenticationInfo用户身份认证对象便创建完成。
返回用户认证对象不为空,说明用户存在,则下一步就是验证用户密码。而用户密码是从数据库中读取出来经过加密的密码,而token中包含的用户密码则是请求传入的明文密码,这个时候就需要进一步进行密码之间的加解密对比认证:
protected void assertCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) throws AuthenticationException {
CredentialsMatcher cm = getCredentialsMatcher(); // 获取之前设置的加密匹配器
if (cm != null) { // 加密匹配器不为空
if (!cm.doCredentialsMatch(token, info)) { // 使用加密匹配器中的加密规则,根据token和Info中的用户密码进行对比认证
String msg = "Submitted credentials for token [" + token + "] did not match the expected credentials.";
throw new IncorrectCredentialsException(msg); // 密码匹配错误,则抛出IncorrectCredentialsException异常
}
} else {
throw new AuthenticationException("A CredentialsMatcher must be configured in order to verify credentials during authentication. If you do not wish for credentials to be examined, you can configure an " + AllowAllCredentialsMatcher.class.getName() + " instance."); // 若不存在加密匹配器(若不设置加密匹配器,则Shiro会默认设置一个,只是没有对应加密规则),则抛出AuthenticationException异常
}
}
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
Object tokenCredentials = getCredentials(token);
Object accountCredentials = getCredentials(info);
return equals(tokenCredentials, accountCredentials); // 密码对比认证
}
到此,如果用户信息认证账号、密码都通过的话,那么就会返回对应的用户认证信息。这时候再回头来看下,最初调用获取用户认证信息的入口方法:
public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {
AuthenticationInfo info;
try {
info = authenticate(token); // 认证通过,获取到用户认证信息
} catch (AuthenticationException ae) {
...
}
Subject loggedIn = createSubject(token, info, subject); // 根据传入token、用户认证信息、当前subject再创建一个新的属于已登录状态的subject
onSuccessfulLogin(token, info, loggedIn);
return loggedIn; // 返回新建的subject
}
返回新建属于已登录状态的subject之后,再回头看获到subject之后的逻辑:
public void login(AuthenticationToken token) throws AuthenticationException {
clearRunAsIdentitiesInternal();
Subject subject = securityManager.login(this, token); // 用户登录认证成功后,获取属于已登录状态的subject
PrincipalCollection principals;
String host = null;
// 获取上面返回的已登录状态的subject中的principals属性(即在realm的doGetAuthenticationInfo方法中封装的用户信息)
if (subject instanceof DelegatingSubject) {
DelegatingSubject delegating = (DelegatingSubject) subject;
principals = delegating.principals;
host = delegating.host;
} else {
principals = subject.getPrincipals();
}
if (principals == null || principals.isEmpty()) {
String msg = "Principals returned from securityManager.login( token ) returned a null or empty value. This value must be non null and populated with one or more elements.";
throw new IllegalStateException(msg);
}
this.principals = principals; // 将获取到principals赋值到subject的principals属性(之后若需要获取用户信息就返回此subject的principals属性即可)
this.authenticated = true; // 将subject标识为已通过登录认证
...
Session session = subject.getSession(false); // 获取session
if (session != null) {
this.session = decorate(session); // 生成一个基于session的代理session,并赋值到subject的session的属性
} else {
this.session = null;
}
}
到此,用户登录认证流程的源码解析完毕。
用户角色、权限认证
@PostMapping("/login")
public String login(@RequestBody User user) {
...
// 校验角色
if (!subject.hasRole("ar")) {
return "用户无ar角色";
}
// 校验权限
if (subject.isPermitted("ap")) {
return "用户无ap权限";
}
...
}
首先来看下用户的角色是如何认证:
public boolean hasRole(String roleIdentifier) {
// 首先需要判断当前是否存在principals(即用户信息)
// 存在用户信息,说明用户已登录,接着再判断用户是否拥有指定角色
return hasPrincipals() && securityManager.hasRole(getPrincipals(), roleIdentifier);
}
确认用户已登录,使用初始化阶段就创建好的authorizer判断用户是否拥有指定角色:
public boolean hasRole(PrincipalCollection principals, String roleIdentifier) {
return this.authorizer.hasRole(principals, roleIdentifier);
}
public boolean hasRole(PrincipalCollection principals, String roleIdentifier) {
assertRealmsConfigured(); // 断言是否存在可认证的realm域
for (Realm realm : getRealms()) { // 遍历realms集合(说明可设置多个realms域)
if (!(realm instanceof Authorizer)) continue;
if (((Authorizer) realm).hasRole(principals, roleIdentifier)) { // 对每个realm都进行角色认证,只要存在一个realm域可通过用户角色认证,就认证成功
return true;
}
}
return false; // 否则认证失败
}
先确保存在可认证的realm域:
protected void assertRealmsConfigured() throws IllegalStateException {
Collection<Realm> realms = getRealms();
if (realms == null || realms.isEmpty()) { // realm域集合为空,抛出异常,中止用户角色认证
String msg = "Configuration error: No realms have been configured! One or more realms must be present to execute an authorization operation.";
throw new IllegalStateException(msg);
}
}
确认存在可认证的realm域后,继续执行角色认证:
public boolean hasRole(PrincipalCollection principal, String roleIdentifier) {
AuthorizationInfo info = getAuthorizationInfo(principal); // 根据用户信息,获取用户授权信息
return hasRole(roleIdentifier, info); // 根据传入的指定角色和获取到的用户授权信息作对比,查看用户是否拥有指定角色
}
如何获取用户授权信息:
protected AuthorizationInfo getAuthorizationInfo(PrincipalCollection principals) {
...
AuthorizationInfo info = null;
Cache<Object, AuthorizationInfo> cache = getAvailableAuthorizationCache(); // 从缓存中获取用户授权信息映射cache
if (cache != null) {
...
Object key = getAuthorizationCacheKey(principals);
info = cache.get(key); // 根据映射cache获取已认证过的用户授权信息
...
}
if (info == null) { // info为空,说明缓存获取失败
info = doGetAuthorizationInfo(principals); // 重点关注。获取用户授权信息
if (info != null && cache != null) {
if (log.isTraceEnabled()) {
log.trace("Caching authorization info for principals: [" + principals + "].");
}
Object key = getAuthorizationCacheKey(principals);
cache.put(key, info); // 将用户授权信息加入缓存
}
}
return info; // 最后返回用户授权信息
}
以上源码中有个doGetAuthorizationInfo方法是否很眼熟呢?不错,此处正是调用了自定义CertificationRealm类中获取用户授权信息的方法:
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); // 创建用户授权信息对象
User user = (User) principals.getPrimaryPrincipal(); // 获取用户名
List<Role> roles = userService.getRolesByAccount(user.getAccount()); // 根据用户名获取对应的角色列表
Set<String> roleIds = new HashSet<>();
roles.forEach(role -> roleIds.add(role.getId())); // 获取角色列表的id,并封装成set集合
authorizationInfo.addRoles(roleIds); // 添加角色id列表
List<Permission> permissions = userService.getPermissionsByAccount(user.getAccount()); // 根据用户名获取对应的权限列表
Set<String> permissionIds = new HashSet<>();
permissions.forEach(permission -> permissionIds.add(permission.getId())); // 获取权限列表的id,并封装成set集合
authorizationInfo.addStringPermissions(permissionIds); // 添加权限id列表
return authorizationInfo; // 返回用户授权信息对象
}
从自定义的doGetAuthorizationInfo方法中可以看到,所谓的用户授权信息,其实就是包含了用户所拥有的的角色和权限信息。
获取到用户授权信息后,下一步就是用户角色的认证:
protected boolean hasRole(String roleIdentifier, AuthorizationInfo info) {
// 所谓的用户角色认证,其实就是根据从数据库读取的用户角色列表中,是否存在指定的角色
return info != null && info.getRoles() != null && info.getRoles().contains(roleIdentifier);
}
最后返回用户角色认证结果true或false,到此,用户角色认证源码解析完成。
因为用户权限的认证和角色认证,源码以及流程都基本一致,在此就不在赘述了,感兴趣的朋友可以自行研究。
获取用户信息
当用户通过认证后,会将从数据库中获取到的用户信息存入subject中。当我们需要获取用户信息时候:
@GetMapping("/getInfo")
public User getInfo() {
User user = (User) SecurityUtils.getSubject().getPrincipal(); // 从subject中获取用户信息
return user;
}
public Object getPrincipal() {
return getPrimaryPrincipal(getPrincipals()); // 从封装过的用户信息对象中获取原生的用户信息
}
private Object getPrimaryPrincipal(PrincipalCollection principals) {
if (!isEmpty(principals)) {
return principals.getPrimaryPrincipal(); // 获取并返回用户信息
}
return null;
}
public Object getPrimaryPrincipal() {
if (isEmpty()) {
return null;
}
return iterator().next(); // 返回用户信息集合中的首个用户
}
public Iterator iterator() {
return asSet().iterator();
}
经过层层的调用,终于到了关键代码:
public Set asSet() {
if (realmPrincipals == null || realmPrincipals.isEmpty()) {
return Collections.EMPTY_SET;
}
Set aggregated = new LinkedHashSet(); // 新建一个保存用户信息的set集合
Collection<Set> values = realmPrincipals.values(); // 从登录认证时候封装了用户信息的realmPrincipals中获取用户信息
for (Set set : values) {
aggregated.addAll(set); // 添加用户信息集合
}
if (aggregated.isEmpty()) {
return Collections.EMPTY_SET;
}
return Collections.unmodifiableSet(aggregated); // 最后返回用户信息set集合
}
通过以上代码可知,获取用户信息其实就是从登录认证时候封装了用户信息的realmPrincipals中获取并返回。
用户注销
还有最后的用户注销源码,先来看下注销的方法入口:
@PostMapping("/logout")
public String logout() {
SecurityUtils.getSubject().logout();
return "注销成功";
}
依然是调用subject的注销方法:
public void logout() {
try {
clearRunAsIdentitiesInternal(); // 清空session中的登录认证信息
this.securityManager.logout(this); // 使用安全管理器调用注销方法
} finally {
// 清空登录记录
this.session = null;
this.principals = null;
this.authenticated = false;
}
}
public void logout(Subject subject) {
if (subject == null) {
throw new IllegalArgumentException("Subject method argument cannot be null.");
}
beforeLogout(subject);
PrincipalCollection principals = subject.getPrincipals(); // 获取用户认证信息
if (principals != null && !principals.isEmpty()) {
if (log.isDebugEnabled()) {
log.debug("Logging out subject with primary principal {}", principals.getPrimaryPrincipal());
}
Authenticator authc = getAuthenticator(); // 获取身份认证器属性
if (authc instanceof LogoutAware) {
((LogoutAware) authc).onLogout(principals); // 注销用户认证信息
}
}
try {
delete(subject); // 删除这个已登录状态的subject
} catch (Exception e) {
...
} finally {
try {
stopSession(subject); // 禁用当前session
} catch (Exception e) {
...
}
}
}
public void onLogout(PrincipalCollection principals) {
super.onLogout(principals); // 发送注销通知
Collection<Realm> realms = getRealms();
if (!CollectionUtils.isEmpty(realms)) {
for (Realm realm : realms) {
if (realm instanceof LogoutAware) {
((LogoutAware) realm).onLogout(principals); // 调用realm注销用户认证信息
}
}
}
}
最后其实调用了自定义CertificationRealm类中的logout方法,该方法我们没有重写,所以调用的是父类CachingRealm的onLogout方法:
public void onLogout(PrincipalCollection principals) {
clearCache(principals); // 清空realm域缓存中的用户登录认证、用户角色、用户权限信息
}
到此,将有关当前登录用户的相关认证信息全部清除,用户注销成功。