【源码分析】shiro认证和授权

引言

JWT与Shiro结构示意:JWT无需Cookie,而Shiro可在任何环境下使用session(其自定义session会话管理
在这里插入图片描述

当同一个realm域分别拆成构造安全数据(放用户私有服务)与获取安全数据(放公共模块)的两个类时,对应的配置文件在构建realm时得return对应的类。

@Bean
public LecRealm getRealm() {
    return new UserRealm(); //这个点很重要  要是获取数据源则 return new LecRealm()
}	

在这里插入图片描述
Subject:主体,代表了当前“用户”,与当前应用交互的任何东西都是Subject,与Subject的所有交互都会委托给SecurityManager。
参考link

认证

1、先执行Shiro自己的Filter链;2、再执行Servlet容器的Filter链(即原始的Filter)。
常用过滤器名:authc(认证之后访问)、anon(直接放行)

org.apache.shiro.web.filter.authc.FormAuthenticationFilter#FormAuthenticationFilter

在这里插入图片描述

isAccessAllowed()-> subject.isAuthenticated() 判断当前session中的subject是否已经登陆过,
没有则执行onAccessDenied()
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        if (this.isLoginRequest(request, response)) { //看是不是配置的loginUrl(默认为"/login.jsp"
            if (this.isLoginSubmission(request, response)) { //看是不是Post请求
                if (log.isTraceEnabled()) {
                    log.trace("Login submission detected.  Attempting to execute login.");
                }
                return this.executeLogin(request, response);
            } else {
                if (log.isTraceEnabled()) {
                    log.trace("Login page view.");
                }
                return true;
            }
        } else {
            if (log.isTraceEnabled()) {
                log.trace("Attempting to access a path which requires authentication.  Forwarding to the Authentication url [" + this.getLoginUrl() + "]");
            }

            this.saveRequestAndRedirectToLogin(request, response); 
            //最终用session.setAttribute(SAVED_REQUEST_KEY, savedRequest);保存非loginUrl的请求 然后重定向到loginUrl
            return false;
        }
    }
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
        AuthenticationToken token = this.createToken(request, response); //最终会创建UsernamePasswordToken
        if (token == null) {
            String msg = "createToken method implementation returned null. A valid non-null AuthenticationToken must be created in order to execute a login attempt.";
            throw new IllegalStateException(msg);
        } else {
            try {
                Subject subject = this.getSubject(request, response); //给该Post请求创建一个subject
                subject.login(token); //最终到realm中执行doGetAuthenticationInfo方法
                return this.onLoginSuccess(token, subject, request, response); //跳转到之前保存的非loginUrl的地址(没有则是successUrl(默认为"/")),return false prevent the chain from continuing:即不让请求达到loginUrl
            } catch (AuthenticationException var5) { //该异常被处理了,就抛不出去了
                return this.onLoginFailure(token, var5, request, response); //return true即login failed, let request continue back to the login page:
            }
        }
    }

兜兜转转还是来到了UsernamePasswordToken

    protected AuthenticationToken createToken(String username, String password,
                                              boolean rememberMe, String host) {
        return new UsernamePasswordToken(username, password, rememberMe, host);
    }

登录部分
在这里插入图片描述

1.subject.login(upToken);
2.DelegatingSubjects中自动委托给securityManager:Subject subject = securityManager.login(this, token);
3.委托给Authenticator执行真正的身份验证:ModularRealmAuthenticator根据realms个数选择是认证方式

在这里插入图片描述

4.调用realm域中的认证:AuthenticationInfo info = realm.getAuthenticationInfo(token);
5_开始之前看下缓存:AuthenticationInfo info = getCachedAuthenticationInfo(token);
5.执行自定义认证:protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken)
public class UserRealm extends LecRealm { //继承关系且getName没有覆盖(以致于放置和获取是同一个Realm域)
    @Autowired
    private ShiroService shiroService;
    @Autowired
    SysUserService sysUserService;

    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof UsernamePasswordToken;
    }

    /**
     * 认证(登录时调用)
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        UsernamePasswordToken upToken = (UsernamePasswordToken) authenticationToken;
        //登录信息
        String username = upToken.getUsername();  //用户名 也就是昵称 唯一的
        String password = String.valueOf(upToken.getPassword());

        //查询用户信息
        SysUserEntity user = sysUserService.queryByUserName(username);

        //账号锁定
        if(user.getStatus() == 0){
            throw new LockedAccountException("账号已被锁定,请联系管理员");
        }

        //账号不存在、密码错误
        if(user == null || !user.getPassword().equals(new Sha256Hash(password, user.getSalt()).toHex())) {
            throw new UnknownAccountException("账号或密码不正确");
        }

        Long userId = user.getUserId();
        //用户权限列表
        Set<String> permsSet = shiroService.getUserPermissions(userId);
        //用户角色列表
        Set<String> rolesSet = shiroService.getUserRoles(userId);

        ProfileResult profileResult = new ProfileResult();
        BeanUtils.copyProperties(user,profileResult);
        profileResult.setPermsSet(permsSet);
        profileResult.setRoleSet(rolesSet);

        //将身份信息user  凭着信息accessToken 放入getName()对应的realm
//        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, accessToken, getName());
        //由于现在认证\授权要分离了,所以权限信息得先放到realm域
        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(profileResult, password, this.getName());  //password要为upToken里的
        return info;
    }

}

会话管理

6.认证执行完成数据源准备完毕,开始创建Subject loggedIn = createSubject(token, info, subject);
7.将authenticated=true
8.save(subject);->委托给DefaultSubjectDAO:saveToSession(subject);
Session session = subject.getSession(false); //false则不创建session

        if (session == null) {
            if (!isEmpty(currentPrincipals)) {
                session = subject.getSession(); //内部实现getSession(true);
                //将subject的Principals存储再subject的session中
                session.setAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY, currentPrincipals);
            }
            // otherwise no session and no principals - nothing to save
        } else {

getSession(true)触发session的创建

9.1 DefaultSessionManager中sessionDAO.create(session); //项目这里的sessionDAO实际类型为RedisSessionDAO
9.2 RedisSessionDAO中进行真正的doCreate:分配sessionId、存储
9.3 依据设置看是否将sessionID放Cookie
9.4 只要session里的内容变了,就会触发update更新保存

在这里插入图片描述

//saveSession的关键代码...
key = keySerializer.serialize(getRedisSessionKey(session.getId()));
value = valueSerializer.serialize(session);
...
this.redisManager.set(key, value, expire);
	@Override
	public void update(Session session) throws UnknownSessionException {
		this.saveSession(session);
	}

补充:
参考link
当Subject.logout()时会自动调用stop方法来销毁会话
在这里插入图片描述
在Servlet容器中,默认使用JSESSIONID Cookie维护会话,且会话默认是跟容器绑定的;在某些情况下(手机等移动客户端不支持Cookie)可能需要使用自己的会话机制,此时我们可以使用DefaultWebSessionManager来维护会话
在这里插入图片描述

授权

对于注解@RequiresPermissions(“product:leccategory:list”),用的是动态代理CglibAopProxy

0.对任意的非放行的请求都得执行最上面的认证流程看是否isAccessAllowed()
1.PermissionAnnotationMethodInterceptor
2.开始委派,委派流程和认证特别像,最终由Authorizer真正执行鉴权。
3.AuthrizingRealm中调用自定义的Realm域授权方法,获取用户拥有的所有权限,看是否包含

在这里插入图片描述

/**
 * 公共的realm
 * @author: hyl
 * @date: 2020/02/08
 **/
public class LecRealm extends AuthorizingRealm {
    public void setName(String name){
        super.setName("LecRealm");
    }

    //授权方法
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        //获取安全数据
        ProfileResult result = (ProfileResult) principalCollection.getPrimaryPrincipal();
        //获取当前用户的所有角色信息
        Set<String> rolesPerms = result.getRoleSet();
        //获取权限信息
        Set<String> permsSet = result.getPermsSet();

        //授予权限信息
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        info.setRoles(rolesPerms);
        info.setStringPermissions(permsSet);
        return info;
    }

    //认证方法
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        return null;
    }
}

PS:SpringBoot集成shiro-redis遇到的问题(java.lang.ClassCastException),谁知和dev-tools有冲突:link

项目方案

对于登录的请求,配置类中直接放行:filterMap.put("/sys/login", “anon”);
省得绕来绕去,咱再自己写异常可以统一处理,然后让前端根据返回的code:21000 进行跳转到登录页

/**
	 * 用户登录
	 */
	@RequestMapping(value = "/sys/login" , method = RequestMethod.POST)
	public R login(@RequestBody SysLoginForm form)throws IOException {
		boolean captcha = sysCaptchaService.validate(form.getUuid(), form.getCaptcha());
		if(!captcha){
			return R.error("验证码不正确");
		}
		UsernamePasswordToken upToken = new UsernamePasswordToken(form.getUsername() , form.getPassword());
		//获取subject
		Subject subject = SecurityUtils.getSubject();

		//调用login方法,进入realm完成认证
		subject.login(upToken);

		//获取sessionId
		String sessionId = (String) subject.getSession().getId();
		return R.ok().put("sessionId",sessionId);
	}

shiro配置

/**
 * Shiro配置
 *
 * @author Mark sunlightcs@gmail.com
 */
@Configuration
public class ShiroConfig {
    //1.创建realm
    @Bean
    public LecRealm getRealm() {
        return new UserRealm(); //这个点很重要
    }

    @Bean("securityManager")
    public SecurityManager securityManager(LecRealm realm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(realm);
//        securityManager.setRememberMeManager(null);

        //将自定义的会话管理器注册到安全管理器中
        securityManager.setSessionManager(sessionManager());
        //将自定义的redis缓存管理器注册到安全管理器中
        securityManager.setCacheManager(cacheManager());

        return securityManager;
    }

    @Bean("shiroFilter")
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
        shiroFilter.setSecurityManager(securityManager);

//        shiroFilter.setLoginUrl("http://localhost:6060/authError?code=1");//未登录跳转的url
//        shiroFilter.setUnauthorizedUrl("http://localhost:6060/authError?code=2");//未授权跳转的url

        Map<String, Filter> filters = new HashMap<>();
         //oauth过滤
//        filters.put("oauth2", new OAuth2Filter());
        shiroFilter.setFilters(filters);

        Map<String, String> filterMap = new LinkedHashMap<>();

        filterMap.put("/sys/register", "anon");
        filterMap.put("/sys/role/listAll", "anon");
        filterMap.put("/webjars/**", "anon");
        filterMap.put("/druid/**", "anon");
        filterMap.put("/app/**", "anon");
        filterMap.put("/sys/login", "anon");
        filterMap.put("/swagger/**", "anon");
        filterMap.put("/v2/api-docs", "anon");
        filterMap.put("/swagger-ui.html", "anon");
        filterMap.put("/swagger-resources/**", "anon");
        filterMap.put("/captcha.jpg", "anon");
        filterMap.put("/aaa.txt", "anon");
//        filterMap.put("/**", "oauth2");
        shiroFilter.setFilterChainDefinitionMap(filterMap);

        return shiroFilter;
    }

    @Bean("lifecycleBeanPostProcessor")
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    //开启对shiro注解的支持
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }

    //-----------------------------------------------
    @Value("${spring.redis.host}")
    private String host = "localhost";
    @Value("${spring.redis.port}")
    private int port=6379;

    /**
     * 1.redis的控制器,操作redis
     */
    public RedisManager redisManager() {
        RedisManager redisManager = new RedisManager();
        redisManager.setHost(host);
        redisManager.setPort(port);
        return redisManager;
    }

    /**
     * 2.sessionDao
     */
    public RedisSessionDAO redisSessionDAO() {
        RedisSessionDAO sessionDAO = new RedisSessionDAO();
        sessionDAO.setRedisManager(redisManager());
        return sessionDAO;
    }

    /**
     * 3.会话管理器
     */
    public DefaultWebSessionManager sessionManager() {
        CustomSessionManager sessionManager = new CustomSessionManager();
//        sessionManager.setSessionIdCookieEnabled(false); //不使能cookie
        sessionManager.setSessionIdUrlRewritingEnabled(false);
        sessionManager.setSessionDAO(redisSessionDAO());
        return sessionManager;
    }

    /**
     * 4.缓存管理器
     */
    public RedisCacheManager cacheManager() {
        RedisCacheManager redisCacheManager = new RedisCacheManager();
        redisCacheManager.setRedisManager(redisManager());
        return redisCacheManager;
    }

}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

星空•物语

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值