SpringBoot整合Shiro框架 && Shiro框架核心源码全面解析

2 篇文章 0 订阅
1 篇文章 0 订阅

本文主要展示内容有二:

  1. SpringBoot项目如何整合Shiro框架,从前端请求用户登录,到后端判断用户、角色、权限认证,再到登录后请求获取用户信息,最后用户注销。
  2. 基于以上整合代码的示例,针对其中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框架过滤器的实例命名:

  1. anon:不认证也可以访问,即该过滤器实例中设置的请求路径,不需要进行用户认证即可访问。
  2. 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域缓存中的用户登录认证、用户角色、用户权限信息
}

到此,将有关当前登录用户的相关认证信息全部清除,用户注销成功。

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值