2.shiro工作原理(以集成springboot为例)

Shiro 提供了与 Web 集成的支持,其通过一个 ShiroFilter 入口来拦截需要安全控制的URL,然后进行相应的控制
ShiroFilter 类似于如 Strut2/SpringMVC 这种 web 框架的前端控制器,是安全控制的入口点,其负责读取配置(如ini 配置文件;springboot可使用注解配置类),然后判断URL 是否需要登录/权限等工作。

一、拦截请求链接

1.shiro提供了一系列的链接过滤器:

注:过滤器一般实现org.apache.shiro.web.filter.authc.AuthenticatingFilter类

2.注入Shiro拦截器工厂类(ShiroFilterFactoryBean),配置链接

拦截器类入口方法是createInstance(),该类的主要作用是:

一、 创建了FilterChainManager,即过滤器管理类,包括2个重要属性

1.1 filters:管理全部链接过滤器,包括身份验证的过滤器,有anon,authcBasic,auchc,user和权限验证的过滤器,有perms,roles,ssl,rest,port。同时自定的过滤器也在FilterChainManager里。值得注意的是,过滤器都是单例的。
1.2 filterChains:过滤链。是一个Map对象,其中key为请求的url,value是一个NamedFilterList对象,存放与该url对应的一系列过滤器

二、将过滤器管理类设置到PathMatchingFilterChainResolver类里,该类负责路径和过滤器链的解析与匹配。根据url找到过滤器链。

@Configuration
public class ShiroConfig {
    /**
     * ShiroFilterFactoryBean 处理拦截资源文件问题。
     * 初始化ShiroFilterFactoryBean的时候需要注入:SecurityManager
     * Filter Chain定义说明 1、一个URL可以配置多个Filter,使用逗号分隔
     *                    2、当设置多个过滤器时,全部验证通过,才视为通过
     *                   3、部分过滤器可指定参数,如perms,roles
     */
    @Bean
    public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        // 必须设置 SecurityManager
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        shiroFilterFactoryBean.setLoginUrl("/toLogin");
        // 登录成功后要跳转的链接
        shiroFilterFactoryBean.setSuccessUrl("/Home");
        // 未授权界面;
        shiroFilterFactoryBean.setUnauthorizedUrl("/403");
        /**
         * 拦截器.
         * 定义shiro过滤链 Map结构
         * Map中key(xml中是指value值)
         */
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
        // 配置不会被拦截的链接 顺序判断,优先匹配
        filterChainDefinitionMap.put("/static/**", "anon");
        //配置需要认证才能访问的链接
        filterChainDefinitionMap.put("/**", "authc");
        // 配置退出过滤器
        filterChainDefinitionMap.put("/logout", "logout");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
    }
    }

3.请求链接解析
Shiro会替代org.springframework.web.filter.DelegatingFilterProxy来实现动态代理。DelegatingFilterProxy过滤器的代理类会实现拦截请求,任何请求都会先经过shiro先过滤,直到成功才会执行javaweb本身的过滤器。源码级讲解。

二、登录认证

1、首先调用Subject.login(token) 进行登录,其会自动委托给SecurityManager
2、SecurityManager负责真正的身份验证逻辑;它会委托给Authenticator 进行身份验证;SecurityManager接口继承了Authenticator,另外还有一个ModularRealmAuthenticator实现,其委托给多个Realm 进行验证,验证规则通过AuthenticationStrategy接口指定
3、Authenticator 才是真正的身份验证者,ShiroAPI 中核心的身份认证入口点,此处可以自定义插入自己的实现;Authenticator 的职责是验证用户帐号,是ShiroAPI 中身份验证核心的入口点:如果验证成功,将返回AuthenticationInfo验证信息;此信息中包含了身份及凭证;如果验证失败将抛出相应的AuthenticationException异常
4、Authenticator 可能会委托给相应的AuthenticationStrategy进行多Realm 身份验证,默认ModularRealmAuthenticator会调用AuthenticationStrategy进行多Realm 身份验证;
5、Authenticator 会把相应的token 传入Realm,从Realm 获取身份验证信息,如果没有返回/抛出异常表示身份验证失败了。此处可以配置多个Realm,将按照相应的顺序及策略进行访问。

1.认证思路

程序先获当前用户的Subject对象,然后判断用户是否已经登录,如果登录则不用做认证,若没有登录,则创建 UsernamePasswordToken对象,将用户名密码传入Subject 的login对象进行检验。

@RequestMapping("/toLogin")
    public String loginLogin(Model model, String username, String password, HttpSession userSession) {
        // 判断用户名和密码是否为空
        if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password)) {
            // 用户名或者密码为空
            model.addAttribute("errorInfo", "用户名或者密码为空");
            return "/login";
        }
        //通过subject进行登录操作
        Subject subject = SecurityUtils.getSubject();
        if(!subject.isAuthenticated()){
            //创建封装了用户名和密码的UsernamePasswordToken对象
            UsernamePasswordToken token = new UsernamePasswordToken(username, password);
            try {
                subject.login(token);
                User user = (User) subject.getPrincipal();
                subject.getSession().setAttribute("user", user);
                subject.getSession().setAttribute("userRole", userRole);
                return "redirect:/Home";
            } catch (AuthenticationException e) {
                e.printStackTrace(); //打印异常错误
                // 用户名或者密码为空
                model.addAttribute("errorInfo", "用户名或者密码不正确");
                return "/login";
            }
        }
        else return "redirect:/Home";
    }

2.深入探究

之前的简介中,已经知道了Realm就是shiro与数据库打交道的对象。
Shiro 从 Realm 获取安全数据(如用户、角色、 权限),即 SecurityManager 要验证用户身份,那么它需要从 Realm 获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作。
先讲Realm的认证方面:
简单地说:**subject.login(token);**这句代码调用到最后,就是调用AuthenticationRealm抽象类中的抽象方法doGetAuthenticationInfo方法,doGetAuthenticationInfo方法会返回SimpleAuthenticationInfo对象,源码级讲解。而该方法就是上面的校验过程中,实现认证的自定义Realm需要实现的方法。我们可以通过继承该类,实现该方法达到自定义。
值得注意的是: 一般继承 AuthorizingRealm(授权)即可;其继承了 AuthenticatingRealm(即身份验证),而且也间接继承了 CachingRealm(带有缓存实现)

@Component
public class AuthRealm extends AuthorizingRealm{
    @Autowired
	private UserService userService;
	@Override
	//登陆认证模块
	protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
		/*
		 * 为Shiro提供真实的用户数据
		 * 1.通过token获取用户名和密码
		 * 2.通过用户名和密码查询用户的真实的信息,真实的密码
		 * 3获取数据后通过info对象返回给shiro安全管理器
		 */
		//强转token为UsernamePasswordToken,才有getUsername等方法
		//此处的token,就是subject.login(token)中的token
		UsernamePasswordToken logintoken = (UsernamePasswordToken) token;
		String  username = logintoken.getUsername();
		//通过用户名查询用户信息
		User user = userService.finuserByUsername(username);
		//密码的比对:
		//通过 AuthenticatingRealm 的 credentialsMatcher 属性来进行的密码的比对!
		//参数:user为数据库得到的对象,user.getUpassword()为数据库中的真实密码,this.getName()为当前的Realm名字,当验证通过时就返回,验证不通过就抛出异常。SimpleAuthenticationInfo还有其他参数,例如设置盐值加密。
		AuthenticationInfo info = new SimpleAuthenticationInfo(user, user.getUpassword(), this.getName()); 
	  return info;
	}
}

还有一点要强调的就是,shiro如何完成密码的比对?
我们知道此时,保存有用户信息的有UsernamePasswordToken和SimpleAuthenticationInfo两个对象,shiro肯定会去取出这两个对象中的信息进行比对。
简单地说:密码的具体比对工作是我们自定义的继承了AuthenticatingRealm父类的自定义
Realm类调用CredentialsMatcher的doCredentialsMatch方法完成的。源码级讲解。

3.多Realm认证

场景:假设某需求涉及使用两个角色分别是:学生、教师。要两者实现分开登录。即需要两个个Realm——StudentRealm和TeacherRealm,分别处理学生、教师的验证功能。
分析:正常情况下,当定义了多个Realm,无论是学生登录,教师登录,都会由这两个Realm共同处理。因为当配置了多个Realm时,我们通常使用的认证器是shiro自带的org.apache.shiro.authc.pam.ModularRealmAuthenticator,其中决定使用哪个Realm的是doAuthenticate()方法,该方法中通过getRealms()获取Realm集合,如果realm只有一个,执行的是doSingleRealmAuthentication方法,如果有多个,走的是doMultiRealmAuthentication方法。所以当我们使用ModularRealmAuthenticator类来配置多个Realm的时候,Shiro会使用我们配置的多个Realm进行认证。
补充:modularRealmAuthenticator是shiro提供的realm管理器,在这里可以设置realm的生效。通过setAuthenticationStrategy来设置多realm的使用规则。如果想自己进一步控制多realm,可以自己实现ModularRealmAuthenticator 。

实现方法:创建一个org.apache.shiro.authc.pam.ModularRealmAuthenticator的子类,并重写doAuthenticate()方法,让特定的Realm完成特定的功能。

1.通过创建一个org.apache.shiro.authc.UsernamePasswordToken的子类,在其中添加一个字段loginType,用来标识登录的类型,即是学生登录、教师登录。

enum  LoginType {
    STUDENT("Student"), TEACHER("Teacher")
    private String type
    private LoginType(String type) {
        this.type = type
    }
    @Override
    public String toString() {
        return this.type.toString()
    }
}

2.新建org.apache.shiro.authc.UsernamePasswordToken的子类UserToken

import org.apache.shiro.authc.UsernamePasswordToken
class UserToken extends UsernamePasswordToken {
    //登录类型,判断是学生登录,教师登录
    private String loginType
    public UserToken(final String username, final String password,String loginType) {
        super(username,password)
        this.loginType = loginType
    }
    public String getLoginType() {
        return loginType
    }
    public void setLoginType(String loginType) {
        this.loginType = loginType
    }
}

3.新建org.apache.shiro.authc.pam.ModularRealmAuthenticator的子类UserModularRealmAuthenticator:

/**
 * 当配置了多个Realm时,我们通常使用的认证器是shiro自带的org.apache.shiro.authc.pam.ModularRealmAuthenticator,其中决定使用的Realm的是doAuthenticate()方法
 *
 * 自定义Authenticator
 * 注意,当需要分别定义处理学生和教师和管理员验证的Realm时,对应Realm的全类名应该包含字符串“Student”“Teacher”。
 * 并且,他们不能相互包含,例如,处理学生验证的Realm的全类名中不应该包含字符串"Teacher"。
 */
class UserModularRealmAuthenticator extends ModularRealmAuthenticator {
    private static final Logger logger = LoggerFactory.getLogger(UserModularRealmAuthenticator.class)
    @Override
    protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken)
            throws AuthenticationException {
        logger.info("UserModularRealmAuthenticator:method doAuthenticate() execute ")
        // 判断getRealms()是否返回为空
        assertRealmsConfigured()
        // 强制转换回自定义的CustomizedToken
        UserToken userToken = (UserToken) authenticationToken
        // 登录类型
        String loginType = userToken?.getLoginType()
        // 所有Realm
        Collection<Realm> realms = getRealms()
        // 登录类型对应的所有Realm
        Collection<Realm> typeRealms = new ArrayList<>()
        for (Realm realm : realms) {
            if (realm?.getName()?.contains(loginType))
                typeRealms?.add(realm)
        }
        // 判断是单Realm还是多Realm
        if (typeRealms?.size() == 1){
            logger.info("doSingleRealmAuthentication() execute ")
            return doSingleRealmAuthentication(typeRealms?.get(0), userToken)
        }
        else{
            logger.info("doMultiRealmAuthentication() execute ")
            return doMultiRealmAuthentication(typeRealms, userToken)
        }
    }
}

4.创建分别处理学生登录和教师登录的StudentShiroRealm,TeacherShiroRealm (即自定义 Realm,这里自行编写代码):
5.在ShiroConfig类中的SecurityManager方法进行相应的配置,当然,以下只是ShiroConfig类中的少部分配置,还有属性的配置没有展示出来。

@Configuration
public class ShiroConfig {
    @Bean
    public SecurityManager securityManager(AuthRealm m) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        //设置realm.
        securityManager.setAuthenticator(modularRealmAuthenticator())
        List<Realm> realms = new ArrayList<>()
        //添加多个Realm
        realms.add(teacherShiroRealm())
        realms.add(studentShiroRealm())
        securityManager.setRealms(realms)
        return securityManager
    }
    /**
     * 系统自带的Realm管理,主要针对多realm
     * */
    @Bean
    public ModularRealmAuthenticator modularRealmAuthenticator(){
        //自己重写的ModularRealmAuthenticator
        UserModularRealmAuthenticator modularRealmAuthenticator = new UserModularRealmAuthenticator()
        modularRealmAuthenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy())
        return modularRealmAuthenticator
    }
    @Bean
    public StudentShiroRealm studentShiroRealm() {
        StudentShiroRealm studentShiroRealm = new StudentShiroRealm()
        return studentShiroRealm
    }
    @Bean
    public TeacherShiroRealm teacherShiroRealm() {
        TeacherShiroRealm teacherShiroRealm = new TeacherShiroRealm()
        return teacherShiroRealm
    }

更多关于多Realm认证的细节,可以参考这位博主的文章。

三、授权(以注解为例)

1、首先调用Subject.isPermitted*/hasRole* 接口,其会委托给SecurityManager,而SecurityManager接着会委托给Authorizer;
2、Authorizer是真正的授权者,如果调用如isPermitted(“user:view”),其首先会通过 PermissionResolver把字符串转换成相应的Permission 实例;
3、在进行授权之前,其会调用相应的Realm 获取Subject 相应的角色/权限用于匹配传入的角色/权限;
4、Authorizer 会判断Realm 的角色/权限是否和传入的匹配,如果有多个Realm,会委托给ModularRealmAuthorizer进行循环判断,如果匹配如isPermitted*/hasRole* 会返回true,否则返回false表示授权失败。

1.开启授权注解使用方式,在shiroconfig类中:

  /**
     * 开启授权注解使用的方式
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;

2.在自定义的realm中实现授权方法

public class AuthRealm extends AuthorizingRealm{
	@Autowired
	private UserService userService;
	@Override
	//权限授权模块
	protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
		User user = (User) principals.getPrimaryPrincipal();
		List<String> listPermission = userService.findAdminRole(user.getUserId());
		SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
		//授予admin权限
		info.addRole("admin");
		return info;
	}
	}

3.编写控制器(测试访问’/user/query’有权限,访问’/user/update’就没有权限)

@Controller
@RequestMapping("/user")
public class UserController{
    @RequiresRoles(value = {admin})
    @RequestMapping("/query")
    public String query()
    {
        return "/user";
    }
    @RequiresRoles(value = {user})
    @RequestMapping("/update")
    public String query()
    {
        return "/user";
    }
}

未完待续。。。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值