爬山的蜗牛旅程:九、springboot+shiro+redis+服务无状态

学习springboot的旅程,就像蜗牛爬山,一点点的往上爬,一点点的欣赏旅途的风景

继续上一章的故事,听说小猿公司要加人,同时又要搞手机端。小猿此刻头大得像个灯泡一样能照亮人间!然后小猿把自己锁进了洗手间,蹲在坑上冥想,性能提升+手机端接入所面临的问题:

  • 服务集群:问题身份验证信息的共享问题?
  • 手机端:没session这个概念?怎么同时兼任解决手机及PC的身份安全验证?

大约几小时后,小猿在众人奇异目光下,兴奋的冲出洗手间,因为他想到了shiro+redis+服务无状态

服务无状态:支业务系统不存储session信息
shiro:安全管理框架,教程请看这
redis:用来共享信息的,比如身份信息

springboot+shiro+redis+服务无状态

  • 第一步:pom.xml引入相关配置
		<!-- shiro -->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.4.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-ehcache</artifactId>
            <version>1.4.0</version>
        </dependency>

        <!-- shiro+redis缓存插件 -->
        <dependency>
            <groupId>org.crazycake</groupId>
            <artifactId>shiro-redis</artifactId>
            <version>2.4.2.1-RELEASE</version>
        </dependency>
  • 第二步:在application.properties(application.ymx)配置
#redis
#请看上一章的redis第二步配置
#shiro
spring.redis.shiro.timeout=0
  • 第三步:实现Realm【继承接口,extends AuthorizingRealm】
import com.example.hxzboot.Dome.Sys.Core.Service.CoreService;
import com.example.hxzboot.Dome.Sys.User.Service.UserService;
import com.example.hxzboot.Dome.Util.UtilClass;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.HashSet;
import java.util.Set;
/**
* 【AuthorizingRealm】接口提供了两个方法(验证权限和验证身份-->自己定义验证逻辑)
*  1-protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollectionprincipalCollection)用来验证权限(授权的)
*  2-protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationTokenauthenticationToken)throws AuthenticationException 用来身份验证的
**/
public class HxzRealm extends AuthorizingRealm {
    //此处注入数据库连接,可通过数据库验证用户和权限验证
    @Autowired
    private CoreService coreService;

    /**
     * 授权--用户权限认证
     * 此处要考虑前后分离和前后不分离的情况
     * 前后分离:请求都是跨域ajax
     * 前后不分离:则是普通请求
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        String username = (String) SecurityUtils.getSubject().getPrincipal();
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        //假设用户拥有的权限--此处我是写死的,后期加权限时可补上
        Set<String> stringSet = new HashSet<>();
        stringSet.add("log/initlog");
        info.setStringPermissions(stringSet);//shiro会将权限集合跟请求地址进行匹配,来验证权限
        return info;
    }

    // 属于user角色@RequiresRoles("user")
    // 必须同时属于user和admin角@RequiresRoles({ "user", "admin" })
    //修饰controller方法的标签--被这标签修饰的方法是被shiro权限管理,会进行权限认证(没有的者不进入权限认证)
    //@RequiresRoles(value = { "user", "admin"})
    //@RequiresPermissions("user:query")



    /**
     * 认证--用户身份验证
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        String userName = (String) authenticationToken.getPrincipal();
        String userPwd = new String((char[]) authenticationToken.getCredentials());//页面的传过来的密码被加密后的

        //假设已经在数据库获取加密的密码
        String password = "1bb2957bf9db25cb4a64ea4023ec8301";//用hxz和123加密的

        if (userName == null) {
            throw new AccountException("用户名不正确");
        }

        return new SimpleAuthenticationInfo(userName, password, ByteSource.Util.bytes(userName + "salt"),getName());
    }
}
  • 第四步:【重写WebSessionManager管理器】【实现无状态管理】–【如果不需要前后分离,可不用此步骤】
import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.util.StringUtils;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.Serializable;

public class HxzSessionManager extends DefaultWebSessionManager {
    private static final String AUTHORIZATION = "Authorization";

    private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";

    public HxzSessionManager() {
        super();
    }

    @Override
    protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
        String id = WebUtils.toHttp(request).getHeader(AUTHORIZATION);
        //如果请求头中有 Authorization 则其值为sessionId
        if (!StringUtils.isEmpty(id)) {
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
            return id;
        } else {
            //否则按默认规则从cookie取sessionId
            return super.getSessionId(request, response);
        }
    }
}
  • 第五步:【核心配置】–【怎么配置已经在方法上说明了】
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.crazycake.shiro.RedisCacheManager;
import org.crazycake.shiro.RedisManager;
import org.crazycake.shiro.RedisSessionDAO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.core.env.Environment;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.LinkedHashMap;
import java.util.Map;

@Configuration
public class ShiroConfig implements WebMvcConfigurer {
    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    @Value("${spring.redis.host}")
    private String host;

    @Value("${spring.redis.port}")
    private int port;

    @Value("${spring.redis.password}")
    private String password;

    @Value("${spring.redis.shiro.timeout}")
    private int timeout;

	 /**
     * 配置shiro的过滤链,过滤不拦截的
     * @return
     */
    @Bean(name = "shiroFilter")
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        //shiroFilterFactoryBean.setLoginUrl("/restlogin");//前后端分离的情况下是不能设置固定登陆url的
        //shiroFilterFactoryBean.setUnauthorizedUrl("/notRole");

        // <!-- authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问-->//---这一块要动态化(加入白名单功能)
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        // 配置不会被拦截的链接 顺序判断,因为前端模板采用了thymeleaf,这里不能直接使用 ("/static/**", "anon")来配置匿名访问,必须配置到每个静态目录
        filterChainDefinitionMap.put("/login/*", "anon");//登陆页面
        filterChainDefinitionMap.put("/restlogin/*", "anon");//基于Rest登陆

        filterChainDefinitionMap.put("/", "anon");
        filterChainDefinitionMap.put("/static/**", "anon");
        filterChainDefinitionMap.put("/templates/**", "anon");

        filterChainDefinitionMap.put("/log/initlog", "anon");

        //主要这行代码必须放在所有权限设置的最后,不然会导致所有 url 都被拦截 剩余的都需要认证
        filterChainDefinitionMap.put("/**", "authc");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;

    }

    /**
     * shiro 安全管理器(核心)-验证和授权都通过它
     * @return
     */
    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(hxzRealm());
        // 自定义session管理 使用redis
        securityManager.setSessionManager(sessionManager());//此处使用自定义sessionManager管理器,实现前后分离--如果不需要前后分离,可注释掉这行代码
        // 自定义缓存实现 使用redis
        securityManager.setCacheManager(cacheManager());//此处开启redis缓存管理--如果不需要redis缓存,可注释掉这行代码
        return securityManager;
    }


    /**
     * 密码加密配置
     * @return
     */
    @Bean(name = "credentialsMatcher")
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        // 散列算法:这里使用MD5算法;
        hashedCredentialsMatcher.setHashAlgorithmName("md5");
        // 散列的次数,比如散列两次,相当于 md5(md5(""));
        hashedCredentialsMatcher.setHashIterations(2);
        // storedCredentialsHexEncoded默认是true,此时用的是密码加密用的是Hex编码;false时用Base64编码
        hashedCredentialsMatcher.setStoredCredentialsHexEncoded(true);
        return hashedCredentialsMatcher;
    }


    @Bean
    public HxzRealm hxzRealm() {
        HxzRealm hxzRealm = new HxzRealm();
        // 告诉realm,使用credentialsMatcher加密算法类来验证密文
        hxzRealm.setCredentialsMatcher(hashedCredentialsMatcher());
        hxzRealm.setCachingEnabled(false);
        return hxzRealm;
    }


    //自定义sessionManager管理器,实现前后分离--如果不需要前后分离,可注释掉
    @Bean
    public SessionManager sessionManager() {
        HxzSessionManager mySessionManager = new HxzSessionManager();
        mySessionManager.setSessionDAO(redisSessionDAO());
        return mySessionManager;
    }

    /**
     * cacheManager 缓存 redis实现 ,如果不需要redis管理,可注释掉
     * <p>
     * 使用的是shiro-redis开源插件
     *
     * @return
     */
    @Bean
    public RedisCacheManager cacheManager() {
        RedisCacheManager redisCacheManager = new RedisCacheManager();
        redisCacheManager.setRedisManager(redisManager());
        return redisCacheManager;
    }

    /**
     * RedisSessionDAO shiro sessionDao层的实现,通过自定义缓存管理器,如果不需要redis管理,可注释掉
     * <p>
     * 使用的是shiro-redis开源插件
     */
    @Bean
    public RedisSessionDAO redisSessionDAO() {
        RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
        redisSessionDAO.setRedisManager(redisManager());
        return redisSessionDAO;
    }

    /**
     * 配置shiro redisManager--自定义缓存管理器,实现redis进行缓存管理,如果不需要redis管理,可注释掉
     * <p>
     * 使用的是shiro-redis开源插件
     *
     * @return
     */
    public RedisManager redisManager() {
        RedisManager redisManager = new RedisManager();
        redisManager.setHost(host);
        redisManager.setPort(port);
        redisManager.setExpire(1800);// 配置缓存过期时间,单位是秒,超过时间自动清空token
        redisManager.setTimeout(timeout);
        redisManager.setPassword(password);
        return redisManager;
    }




    //---以下配置是加入注解,进行更细粒度权限控制

    /**
     * 配置Shiro生命周期处理器--此处使用了static 静态注入,否则@Value标签不生效
     * @return
     */
    @Bean
    public static LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }


    /**
     * 下面的代码是添加注解支持
     * @return
     */
    @Bean
    @DependsOn("lifecycleBeanPostProcessor")
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
        return defaultAdvisorAutoProxyCreator;
    }

    /**
     * 开启shiro aop注解支持.
     * 使用代理方式;所以需要开启代码支持;
     * @param securityManager
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }

}
  • 第六步:登陆验证
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.subject.Subject;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;

@RestController
@RequestMapping("/restlogin")
public class RestLoginController {

    @RequestMapping(value = "/login", method = {RequestMethod.POST,RequestMethod.GET})
    public String rsdome(@RequestParam("username") String username, @RequestParam("password") String password, HttpServletRequest request) {
        // 从SecurityUtils里边创建一个 subject
        Subject subject = SecurityUtils.getSubject();
        // 在认证提交前准备 token(令牌)
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);
        // 执行认证登陆
        try {
            subject.login(token);
        } catch (UnknownAccountException uae) {
            return "未知账户";
        } catch (IncorrectCredentialsException ice) {
            return "密码不正确";
        } catch (LockedAccountException lae) {
            return "账户已锁定";
        } catch (ExcessiveAttemptsException eae) {
            return "用户名或密码错误次数过多";
        } catch (AuthenticationException ae) {
            return "用户名或密码不正确!";
        }
        if (subject.isAuthenticated()) {
            return "登录成功,token="+ subject.getSession().getId();
        } else {
            token.clear();
            return "登录失败";
        }
    }
}
  • 第七步:怎么获取当前登陆用户
String username = (String) SecurityUtils.getSubject().getPrincipal();

好了,大体上实现无状态的服务集群+redis+shiro安全认证,既提升系统并发能力,又兼容PC和手机端的身份安全认证。这时小猿开心得头都秃了(事实上头早秃了!!)。小猿关上电脑,准备回家打王者。就在这时诡异的事情发生了,经理又来电了,说:“小猿啊,公司系统支步支持定时任务啊。。。。”

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值