Spring boot+Shiro

一、Shiro框架

1、官方文档:http://shiro.apache.org/index.html

shrio是一个基于Java的安全认证框架,是一个轻量级的安全框架,主要的作用是在后端承担认证和授权的工作,可在JavaSE和JavaEE环境中使用

2、shiro架构

在这里插入图片描述

  • Authentication:有时称为“登录”,这是证明用户是他们所说的身份的行为。
  • Authorization:**访问控制的过程,即确定“谁”有权访问“什么”。
  • Session Management:即使在非Web或EJB应用程序中,也管理用户特定的会话。
  • Cryptography:使用密码算法保持数据安全,同时仍然易于使用。

在不同的应用程序环境中,还具有其他功能来支持和加强这些问题,尤其是:

  • Web Support:Shiro的Web支持API可帮助轻松保护Web应用程序。
  • Caching:缓存是Apache Shiro API的第一层公民,可确保安全操作保持快速有效。
  • Concurrency:Apache Shiro的并发功能支持多线程应用程序。
  • Testing:测试支持可以帮助您编写单元测试和集成测试,并确保您的代码将按预期进行保护。
  • “Run As”:一种功能,允许用户采用其他用户的身份(如果允许),有时在管理方案中很有用。
  • “Remember Me”:在整个会话中记住用户的身份,因此他们仅在必要时登录。

在认证过程中有三个核心的对象:Subject,SecurityManager和Realm(s)
在这里插入图片描述

  • Subject:Subject本质上是当前正在执行的用户的安全特定“视图”,是shiro对外的API核心。Subject代表了当前的用户,可以是一个人,但它也可以表示第三方服务,守护程序帐户。

    Subject只是一个门面,其内部有关操作都交给了SecurityManager去执行

  • SecurityManager:是Shiro体系结构的核心,并充当一种“伞”对象,该对象协调其内部安全组件,这些安全组件一起形成对象图。相当于sprintmvc的dispatcherServlet,负责其他组件与shiro的交互

  • Realm:领域充当Shiro与应用程序的安全数据之间的“桥梁”或“连接器”。可以执行身份验证(登录)和授权(访问控制),相当于数据库中的DataSource,可以自定义该类

二、在Springboot中配置shiro

1、导包

<!--shiro-->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>1.5.3</version>
</dependency>
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring-boot-starter</artifactId>
    <version>1.5.3</version>
</dependency>
<!--shiro注解支持需要aop的支持-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

2、开启aop的自动代理

sprint:
	aop:
		proxy-target-class: true

3、自定义Realm

@Slf4j
public class ShiroRealm extends AuthorizingRealm {
    @Lazy
    @Resource
    private RedisUtil redisUtil;
    @Autowired
    private SysUserDao sysUserDao;

    /**
     * 重写身份令牌验证方式
     * 若返回false则在执行登录操作时会报org.apache.shiro.authc.pam.UnsupportedTokeException
     * 即不支持的身份令牌
     */
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JwtToken;
    }

    /**
     * 权限信息认证(包括角色认证以及权限认证):是用户访问controller的时候才进行验证
     * 触发检测用户权限时才会调用此方法,例如checkRole,checkPermission
     *
     * @param principals 身份信息
     * @return AuthorizationInfo 权限信息
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        return null;
    }

    /**
     * 用户信息认证:在用户进行登录的时候进行验证
     *
     * @param auth 用户登录的账号密码信息
     * @return 返回封装了用户信息的 AuthenticationInfo 实例
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
        String token = (String) auth.getCredentials();
        if (token == null) {
            throw new AuthenticationException("token为空!");
        }
        
        // 校验token有效性
        LoginUser loginUser = this.checkUserTokenIsEffect(token);
        //可进行一系列验证。。。
        
        /**
         * 封装用户的登录数据
         */
        return new SimpleAuthenticationInfo(loginUser, token, getName());
    }

    /**
     * 清除当前用户的权限认证缓存
     *
     * @param principals 权限信息
     */
    @Override
    public void clearCache(PrincipalCollection principals) {
        super.clearCache(principals);
    }

}
SimpleAuthenticationInfo源码解析
  • 构造方法的第一个参数指定了登录的用户数据
  • 第二个参数用来校验第一个参数所指定的用户,一般可为用户的token或password
  • 第三个参数指定了授权的对象名
/**
 * Constructor that takes in a single 'primary' principal of the account and its corresponding credentials,
 * associated with the specified realm.
 * <p/>
 * This is a convenience constructor and will construct a {@link PrincipalCollection PrincipalCollection} based
 * on the {@code principal} and {@code realmName} argument.
 *
 * @param principal   the 'primary' principal associated with the specified realm.
 * @param credentials the credentials that verify the given principal.
 * @param realmName   the realm from where the principal and credentials were acquired.
 */
public SimpleAuthenticationInfo(Object principal, Object credentials, String realmName) {
    this.principals = new SimplePrincipalCollection(principal, realmName);
    this.credentials = credentials;
}

4、shiro的配置文件

@Slf4j
@Configuration
public class ShiroConfig {

    /**
     * 开启shiro注解支持
     * @return
     */
    @Bean
    public static LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    /**
     * 添加注解支持
     * @return
     */
    @Bean
    @DependsOn("lifecycleBeanPostProcessor")
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
        defaultAdvisorAutoProxyCreator.setUsePrefix(true);
        return defaultAdvisorAutoProxyCreator;
    }

    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        //设置安全管理器
        advisor.setSecurityManager(securityManager);
        return advisor;
    }

    /**
     * ShiroFilterFactoryBean 处理拦截资源文件问题。
     * 1、一个URL可以配置多个Filter,使用逗号分隔
     * 2、当设置多个过滤器时,全部验证通过,才视为通过
     * 3、部分过滤器可指定参数,如perms,roles
     */
    @Bean
    public ShiroFilterFactoryBean shirFilter(@Qualifier("defaultWebSecurityManager") DefaultWebSecurityManager defaultWebSecurityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        // 设置安全管理器SecurityManager
        shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
        /**
         * 配置拦截器,顺序拦截,使用链式结构
         * anon:无需认证就可以访问
         * authc:需要认证才可以访问
         * user:需要设置remenberMe=true属性才能访问
         * perms:需要拥有对某个资源的权限才能访问
         * role:需要拥有某个角色的权限才能访问
         */
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
        filterChainDefinitionMap.put("/**", "anon");
        //设置shiro的拦截器
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }

    @Bean
    public DefaultWebSecurityManager defaultWebSecurityManager(@Qualifier("shiroRealm") ShiroRealm shiroRealm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        //关联Realm
        securityManager.setRealm(shiroRealm);
        return securityManager;
    }
    /**
     * 自定义身份认证Realm
     * @return
     */
    @Bean
    public ShiroRealm shiroRealm() {
        return new ShiroRealm();
    }
}

三、Shiro的认证过程

首先是先将接收到的用户输入的数据生成UsernamePasswordToken类或者该类的子类,获取shiro的Subject对象执行subject.login(usernamePasswordToken)方法

Subject subject = SecurityUtils.getSubject();
    UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(user.get("email").toString(), user.get("password").toString());
     try {
        subject.login(usernamePasswordToken);
     } catch  (UnknownAccountException uae) {
     	return ResponseEntity.ok(new ResultOK(500,"账号不存在!"));
     } catch (IncorrectCredentialsException ice) {
     	return ResponseEntity.ok(new ResultOK(500,"账号或密码有误!"));
     } catch (LockedAccountException lae) {
     	return ResponseEntity.ok(new ResultOK(500,"账号被锁定!"));
     } catch (AuthenticationException ae) {
     	return ResponseEntity.ok(new ResultOK(500,"用户认证失败!"));
     } catch (Exception e) {
     	return ResponseEntity.ok(new ResultOK(500,e.getMessage()));
     }
     return ResponseEntity.ok(new ResultOK(200,"登录成功!");

这里的usernamePasswordToken(以下简称token)就是用户名和密码的一个结合对象,然后调用subject的login方法将token传入开始认证过程。底层调用的其实是securityManager的login方法:

Subject subject = securityManager.login(this, token);

再往下看securityManager的login方法内部:

public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {
    AuthenticationInfo info;
     try {
            info = authenticate(token);
     } catch (AuthenticationException ae) {
            try {
                onFailedLogin(token, ae, subject);
     } catch (Exception e) {
                if (log.isInfoEnabled()) {
                    log.info("onFailedLogin method threw an " +
                            "exception.  Logging and propagating original AuthenticationException.", e);
     }
            }
            throw ae; //propagate
     }
        Subject loggedIn = createSubject(token, info, subject);
     onSuccessfulLogin(token, info, loggedIn);
     return loggedIn;
}

上面代码的关键在于:

info = authenticate(token);

即将token传入authenticate方法中得到一个AuthenticationInfo类型的认证信息。以下是authenticate方法的具体内容:

public final AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
    if (token == null) {
        throw new IllegalArgumentException("Method argument (authentication token) cannot be null.");
    }
    log.trace("Authentication attempt received for token [{}]", token);
    AuthenticationInfo info;
    try {
        info = doAuthenticate(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) {
        AuthenticationException ae = null;
    if (t instanceof AuthenticationException) {
            ae = (AuthenticationException) t;
    }
        if (ae == null) {
            //Exception thrown was not an expected AuthenticationException.  Therefore it is probably a little more
    //severe or unexpected.  So, wrap in an AuthenticationException, log to warn, and propagate: String msg = "Authentication failed for token submission [" + token + "].  Possible unexpected " +
                    "error? (Typical or expected login exceptions should extend from AuthenticationException).";
    ae = new AuthenticationException(msg, t);
    if (log.isWarnEnabled())
                log.warn(msg, t);
    }
        try {
            notifyFailure(token, ae);
    } catch (Throwable t2) {
            if (log.isWarnEnabled()) {
                String msg = "Unable to send notification for failed authentication attempt - listener error?.  " +
                        "Please check your AuthenticationListener implementation(s).  Logging sending exception " +
                        "and propagating original AuthenticationException instead...";
    log.warn(msg, t2);
    }
        }
        throw ae;
    }
    log.debug("Authentication successful for token [{}].  Returned account [{}]", token, info);
    notifySuccess(token, info);
    return info;
}

首先就是判断token是否为空,不为空再将token传入doAuthenticate方法中:

protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
    assertRealmsConfigured();
    Collection<Realm> realms = getRealms();
    if (realms.size() == 1) {
        return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken);
    } else {
        return doMultiRealmAuthentication(realms, authenticationToken);
    }
}

这一步是判断是有单个Reaml验证还是多个Reaml验证,单个就执行doSingleRealmAuthentication()方法,多个就执行doMultiRealmAuthentication()方法。
一般情况下是单个验证:

protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken token) {
    if (!realm.supports(token)) {
        String msg = "Realm [" + realm + "] does not support authentication token [" +
                token + "].  Please ensure that the appropriate Realm implementation is " +
                "configured correctly or that the realm accepts AuthenticationTokens of this type.";
        throw new UnsupportedTokenException(msg);
    }
    AuthenticationInfo info = realm.getAuthenticationInfo(token);
    if (info == null) {
        String msg = "Realm [" + realm + "] was unable to find account data for the " +
                "submitted AuthenticationToken [" + token + "].";
        throw new UnknownAccountException(msg);
    }
    return info;
}

这一步中首先判断是否支持Realm,只有支持Realm才调用realm.getAuthenticationInfo(token)获取info。

public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
    AuthenticationInfo info = getCachedAuthenticationInfo(token);
    if (info == null) {
        //otherwise not cached, perform the lookup:
        info = doGetAuthenticationInfo(token);
        log.debug("Looked up AuthenticationInfo [{}] from doGetAuthenticationInfo", info);
        if (token != null && info != null) {
            cacheAuthenticationInfoIfPossible(token, info);
        }
    } else {
        log.debug("Using cached authentication info [{}] to perform credentials matching.", info);
    }
    if (info != null) {
        assertCredentialsMatch(token, info);
    } else {
        log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}].  Returning null.", token);
    }
    return info;
}

首先查看Cache中是否有该token的info,如果有,则直接从Cache中去即可。如果是第一次登录,则Cache中不会有该token的info,需要调用doGetAuthenticationInfo(token)方法获取,并将结果加入到Cache中,方便下次使用。而这里调用的doGetAuthenticationInfo()方法就是我们在自定义的Realm中重写的方法,具体的内容是自定义了对拿到的这个token的一个处理的过程:

protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
    if (authenticationToken.getPrincipal() == null)
        return null;
    String email = authenticationToken.getPrincipal().toString();
    User user = userService.findByEmail(email);
    if (user == null)
        return null;
    else return new SimpleAuthenticationInfo(email, user.getPassword(), getName());
}

这其中进行了几步判断:可以判断传入的用户名是否为空,判断传入的用户名在本地的数据库中是否存在,该用户是否有效等验证,验证不通过则返回一个Exception。通过之后则生成一个包括传入用户名和密码的info,注意此时关于用户名的验证已经完成,接下来进入对密码的验证。
将这一步得到的info返回给getAuthenticationInfo方法中的

assertCredentialsMatch(token, info);

此时的info是正确的用户名和密码的信息,token是输入的用户名和密码的信息,经过前面步骤的验证过程,用户名此时已经是真是存在的了,这一步就是验证输入的用户名和密码的对应关系是否正确。

protected void assertCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) throws AuthenticationException {
    CredentialsMatcher cm = getCredentialsMatcher();
    if (cm != null) {
        if (!cm.doCredentialsMatch(token, info)) {
            //not successful - throw an exception to indicate this:
            String msg = "Submitted credentials for token [" + token + "] did not match the expected credentials.";
            throw new IncorrectCredentialsException(msg);
        }
    } 
    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.");
    }
}

这一步验证完成之后,整个shrio认证的过程就结束了。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值