shiro搭建总体流程总结

整体的搭建流程:

总体的代码结构如图:

image

  1. 自定义Realm

    重写授权和认证方法,doGetAuthorizationInfo和doGetAuthenticationInfo方法,并且设置密码加密匹配算法,设置开启缓存。

    /**
     * @author :RealGang
     * @description:自定义权限匹配和密码匹配,认证用户,授权
     * @date : 2021/10/18 18:04
     */
    public class MyShiroRealm extends AuthorizingRealm {
        private final static Logger logger = LoggerFactory.getLogger(MyShiroRealm.class);
        /**
         * 延迟加载bean,解决缓存Cache不能正常使用;事务Transaction注解不能正常运行
         */
        @Autowired
        @Lazy
        private UserServiceImpl userService;
    
    
        public MyShiroRealm() {
            //设置凭证匹配器,修改为hash凭证匹配器
            HashedCredentialsMatcher myCredentialsMatcher = new HashedCredentialsMatcher();
            //设置算法
            myCredentialsMatcher.setHashAlgorithmName("md5");
            //散列次数
            myCredentialsMatcher.setHashIterations(1024);
            this.setCredentialsMatcher(myCredentialsMatcher);
            //开启缓存
            this.setCachingEnabled(true);
            this.setAuthenticationCachingEnabled(true);
            this.setAuthorizationCachingEnabled(true);
        }
    
        /**
         * @description: 授权;doGetAuthorizationInfo方法是在我们调用;SecurityUtils.getSubject().isPermitted()这个方法,授权后用户角色及权限会保存在缓存中的
         *  "@RequiresPermissions"这个注解其实就是在执行SecurityUtils.getSubject().isPermitted()
         *  授权
         *  这个方法在每次访问ShiroConfig里面配置的受保护资源时都会调用
         *  因此,需要做缓存
         * @param: principalCollection
         * @return: org.apache.shiro.authz.AuthorizationInfo
         * @author: RealGang
         * @date: 2021/10/19
         */
        @Override
        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
            User user;
            Object object = principalCollection.getPrimaryPrincipal();
            // 这里用json转化为USER,因为可能从redis获取的用户信息反序列化不能强制转换为user报错
            if (object instanceof User) {
                user = (User) object;
            } else {
                user = JSON.parseObject(JSON.toJSON(object).toString(), User.class);
            }
            String username = user.getUsername();
            SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
    //        User user = (User) principalCollection.fromRealm(this.getClass().getName()).iterator().next();
            //查询数据库
            user = userService.findUserInfo(user.getUsername());
            logger.info("##################执行Shiro权限授权##################user info is:{}" + JSONObject.toJSONString(user));
            Set<String> userPermissions = new HashSet<String>();
            Set<String> userRoles = new HashSet<String>();
            for (Role role : user.getRoles()) {
                userRoles.add(role.getRoleName());
                List<Permission> rolePermissions = role.getPermissions();
                for (Permission permission : rolePermissions) {
                    userPermissions.add(permission.getPermName());
                }
            }
            //角色名集合
            info.setRoles(userRoles);
            //权限名集合,将权限放入shiro中,
            // 这里可以把url,按钮,菜单,api等当做资源来进行权限控制,从而对用户进行权限控制
            info.addStringPermissions(userPermissions);
    
            return info;
        }
    
        /**
         * @description: 认证,登录,doGetAuthenticationInfo这个方法是在用户登录的时候调用的;也就是执行SecurityUtils.getSubject().login()的时候调用;(即:登录验证),验证通过后会用户保存在缓存中的
         * @param: authenticationToken
         * @return: org.apache.shiro.authc.AuthenticationInfo
         * @author: RealGang
         * @date: 2021/10/19
         */
        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
            logger.info("##################执行Shiro登录认证##################");
            // 客户端传来的 username 和 password 会自动封装到 token,先根据 username 进行查询.如果返回 null,则表示用户名错误,直接 return null 即可,Shiro 会自动抛出 UnknownAccountException 异常。
    
            if(authenticationToken==null){
                return null;
            }
            String principal = (String) authenticationToken.getPrincipal();
            //查询数据库
            User user = userService.findByUserName(principal);
            //放入shiro.调用CredentialsMatcher检验密码
            if (user != null) {
                // 若存在,将此用户存放到登录认证info中,无需自己做密码对比,Shiro会为我们进行密码对比校验
    
                // 第三个参数一般也可以是ByteSource.Util.bytes(shiroUser.getUserName()+shiroPasswordService.getPublicSalt())
                // //由于shiro-redis插件需要从这个属性中获取id作为redis的key,所有这里传的是user而不是username
    //            return new SimpleAuthenticationInfo(user, user.getPassWord(), credentialsSalt, this.getClass().getName());
                return new SimpleAuthenticationInfo(user, user.getPassword(), new CurrentSalt(user.getSalt()), this.getClass().getName());
            }
            return null;
        }
    }
    
  2. 添加Shiro自定义会话

    添加自定义会话ID生成器

    这里配置token以"login_token"开头的token也就是sessionId

    public class ShiroSessionIdGenerator implements SessionIdGenerator {
    
        /**
         *实现SessionId生成
         * @param session
         * @return
         */
        @Override
        public Serializable generateId(Session session) {
            Serializable sessionId = new JavaUuidSessionIdGenerator().generateId(session);
            return String.format("login_token_%s", sessionId);
        }
    }
    

    添加自定义会话管理器

    public class ShiroSessionManager extends DefaultWebSessionManager {
    
        //定义常量
        private static final String AUTHORIZATION = "Authorization";
        private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";
        //重写构造器
        public ShiroSessionManager() {
            super();
            this.setDeleteInvalidSessions(true);
        }
    
        /**
         * 重写方法实现从请求头获取Token便于接口统一
         *      * 每次请求进来,
         *      Shiro会去从请求头找Authorization这个key对应的Value(Token)
         * @param request
         * @param response
         * @return
         */
        @Override
        public Serializable getSessionId(ServletRequest request, ServletResponse response) {
            String token = WebUtils.toHttp(request).getHeader(AUTHORIZATION);
            //如果请求头中存在token 则从请求头中获取token
            if (!StringUtils.isEmpty(token)) {
                request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE);
                request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, token);
                request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
                return token;
            } else {
                // 这里禁用掉Cookie获取方式
                return null;
                // 否则按默认规则从cookie取sessionId
                //return super.getSessionId(request, response);
            }
        }
    }
    
  3. 配置shiro:shiroConfig

    在该配置文件里主要是一个filterFactoryBean,该类里注册SecurityManager,并且可以设置一些自定义过滤器,然后设置过滤url规则,在securityManager方法里注入自定义的realm,并且注入自己重写的redisCacheManager和会话管理器sessionManager

    @Configuration
    public class ShiroConfig {
    
        // CACHE_KEY里是缓存AuthenticationInfo信息和AuthorizationInfo信息的缓存名称的前缀
        private static final String CACHE_KEY = "shiro:cache:";
        private static final String SESSION_KEY = "shiro:session:";
        private static final int EXPIRE = 18000;
        @Value("${spring.redis.host}")
        private String host;
        @Value("${spring.redis.port}")
        private int port;
        @Value("${spring.redis.timeout}")
        private int timeout;
    //    @Value("${spring.redis.password}")
    //    private String password;
    
        @Value("${spring.redis.jedis.pool.min-idle}")
        private int minIdle;
        @Value("${spring.redis.jedis.pool.max-idle}")
        private int maxIdle;
        @Value("${spring.redis.jedis.pool.max-active}")
        private int maxActive;
    
        //开启对shior注解的支持
        @Bean
        public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(org.apache.shiro.mgt.SecurityManager securityManager) {
            AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
            authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
            return authorizationAttributeSourceAdvisor;
        }
    
        /**
         * @description: 自定义过滤器 MyShiroRealm,我们的业务逻辑全部定义在这个 bean 中。
         * @param:
         * @return: com.example.autohomingtest.config.MyShiroRealm
         * @author: RealGang
         * @date: 2021/10/19
         */
        @Bean
        public MyShiroRealm myShiroRealm(){
            return new MyShiroRealm();
        }
    
    
        /**
         * @description: 将 myShiroRealm 注入到 DefaultWebSecurityManager bean 中,完成注册。
         * @param: myShiroRealm
         * @return: org.apache.shiro.web.mgt.DefaultWebSecurityManager
         * @author: RealGang
         * @date: 2021/10/19
         */
        @Bean
        public SecurityManager securityManager(@Qualifier("myShiroRealm") MyShiroRealm myShiroRealm){
            DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
            manager.setRealm(myShiroRealm);
            manager.setCacheManager(redisCacheManager());
            // 这里是我自己增加的,不然sessionManager没有注册进去
            manager.setSessionManager(sessionManager());
            SecurityUtils.setSecurityManager(manager);
            return manager;
        }
    
        /**
         * @description: ShiroFilterFactoryBean,这是 Shiro 自带的一个 Filter 工厂实例,所有的认证和授权判断都是由这个 bean 生成的 Filter 对象来完成的,
         * 这就是 Shiro 框架的运行机制,开发者只需要定义规则,进行配置,具体的执行者全部由 Shiro 自己创建的 Filter 来完成。
         * @param: manager
         * @return: org.apache.shiro.spring.web.ShiroFilterFactoryBean
         * @author: RealGang
         * @date: 2021/10/19
         */
        @Bean
        public ShiroFilterFactoryBean filterFactoryBean(@Qualifier("securityManager") SecurityManager securityManager){
            ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
            factoryBean.setSecurityManager(securityManager);
            Map<String, Filter> filterMap = factoryBean.getFilters();
            // 这里把自定义拦截OPTIONS请求的拦截器注册进来
            filterMap.put("authc",new MyShiroFilter());
            Map<String,String> map = new LinkedHashMap<>();
            /**
             * Shiro 内置过滤器,过滤链定义,从上向下顺序执行
             *  常用的过滤器:
             *      anon:无需认证(登录)可以访问
             *      authc:必须认证才可以访问
             *      user:只要登录过,并且记住了密码,如果设置了rememberMe的功能可以直接访问
             *      perms:该资源必须得到资源权限才可以访问
             *      role:该资源必须得到角色的权限才可以访问
             */
            map.put("/manage","perms[manage]");
            map.put("/administrator","roles[administrator]");
            //anon表示可以匿名访问
            map.put("/index", "anon");
            map.put("/login", "anon");
            map.put("/static/**", "anon");
            map.put("/user/testDb","anon");
            //authc表示需要登录
            map.put("/user/**","authc");
            map.put("/main","authc");
            factoryBean.setFilterChainDefinitionMap(map);
            //设置登录页面,覆盖默认的登录url,这里如果未认证会跳转到/unauthc这里来
            factoryBean.setLoginUrl("/unauthc");
            //未授权页面
            factoryBean.setUnauthorizedUrl("/unauthr");
            // 登录成功后要跳转的链接
            factoryBean.setSuccessUrl("/index");
            return factoryBean;
        }
    
    
        /**
         * 配置Redis管理器
         * @Attention 使用的是shiro-redis开源插件
         * @return
         */
        @Bean
        public RedisManager redisManager() {
            RedisManager redisManager = new RedisManager();
            redisManager.setHost(host);
            redisManager.setPort(port);
            redisManager.setTimeout(timeout);
    //        redisManager.setPassword(password);
            JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
            jedisPoolConfig.setMaxTotal(maxIdle+maxActive);
            jedisPoolConfig.setMaxIdle(maxIdle);
            jedisPoolConfig.setMinIdle(minIdle);
            redisManager.setJedisPoolConfig(jedisPoolConfig);
            return redisManager;
        }
    
    
        @Bean
        public RedisCacheManager redisCacheManager() {
            RedisCacheManager redisCacheManager = new RedisCacheManager();
            redisCacheManager.setRedisManager(redisManager());
            redisCacheManager.setKeyPrefix(CACHE_KEY);
            // shiro-redis要求放在session里面的实体类必须有个id标识
            //这是组成redis中所存储数据的key的一部分,完整的key的形式:shiro:cache:com.example.autohomingtest.config.MyShiroRealm.authenticationCache:username
            redisCacheManager.setPrincipalIdFieldName("username");
            return redisCacheManager;
        }
    
        /**
         * SessionID生成器
         *
         */
        @Bean
        public ShiroSessionIdGenerator sessionIdGenerator(){
            return new ShiroSessionIdGenerator();
        }
    
        /**
         * 配置RedisSessionDAO
         */
        @Bean
        public RedisSessionDAO redisSessionDAO() {
            RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
            redisSessionDAO.setRedisManager(redisManager());
            redisSessionDAO.setSessionIdGenerator(sessionIdGenerator());
            redisSessionDAO.setKeyPrefix(SESSION_KEY);
            redisSessionDAO.setExpire(EXPIRE);
            return redisSessionDAO;
        }
    
        /**
         * 配置Session管理器
         * @Author Sans
         *
         */
        @Bean
        public SessionManager sessionManager() {
            ShiroSessionManager shiroSessionManager = new ShiroSessionManager();
            shiroSessionManager.setSessionDAO(redisSessionDAO());
            //禁用cookie
            shiroSessionManager.setSessionIdCookieEnabled(false);
            //禁用会话id重写
            // ession管理器的setSessionIdUrlRewritingEnabled(false)配置没有生效,导致没有认证直接访问受保护资源出现多次重定向的错误。将shiro版本切换为1.5.0后就解决了这个bug。
            shiroSessionManager.setSessionIdUrlRewritingEnabled(false);
            return shiroSessionManager;
        }
    }
    
  4. 解决跨域问题

    配置CorConfig:

    @Configuration
    public class CorsConfig implements WebMvcConfigurer {
    
        @Override
        public void addCorsMappings(CorsRegistry registry) {
            registry.addMapping("/**")
                    .allowedOriginPatterns("*")
                    .allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS")
                    .allowCredentials(true)
                    .maxAge(3600)
                    .allowedHeaders("*");
        }
    }
    

    配置MyShiroFilter过滤器,这里主要拦截OPTIONS请求,并且注册到上述的filterFactoryBean中去:

    public class MyShiroFilter extends FormAuthenticationFilter {
        @Override
        protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
            if (request instanceof HttpServletRequest) {
                if (((HttpServletRequest)request).getMethod().toUpperCase().equals("OPTIONS")) {
                    return true;
                }
            }
    //        if (request.getAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID)!=null) {
    //            return true;
    //        }
            return super.isAccessAllowed(request, response, mappedValue);
        }
    }
    
  5. 自定义盐值生成方法保证可以序列化(由于shiro当中的ByteSource没有实现序列化接口,缓存时会发生错误,因此,我们需要通过自定义ByteSource的方式实现这个接口):

    public class CurrentSalt implements ByteSource, Serializable {
        private static final long serialVersionUID = 125096758372084309L;
    
        private  byte[] bytes;
        private String cachedHex;
        private String cachedBase64;
    
        public CurrentSalt(){
        }
    
        public CurrentSalt(byte[] bytes) {
            this.bytes = bytes;
        }
    
        public CurrentSalt(char[] chars) {
            this.bytes = CodecSupport.toBytes(chars);
        }
    
        public CurrentSalt(String string) {
            this.bytes = CodecSupport.toBytes(string);
        }
    
        public CurrentSalt(ByteSource source) {
            this.bytes = source.getBytes();
        }
    
        public CurrentSalt(File file) {
            this.bytes = (new CurrentSalt.BytesHelper()).getBytes(file);
        }
    
        public CurrentSalt(InputStream stream) {
            this.bytes = (new CurrentSalt.BytesHelper()).getBytes(stream);
        }
    
        public static boolean isCompatible(Object o) {
            return o instanceof byte[] || o instanceof char[] || o instanceof String || o instanceof ByteSource || o instanceof File || o instanceof InputStream;
        }
    
        public void setBytes(byte[] bytes) {
            this.bytes = bytes;
        }
    
        @Override
        public byte[] getBytes() {
            return this.bytes;
        }
    
    
        @Override
        public String toHex() {
            if(this.cachedHex == null) {
                this.cachedHex = Hex.encodeToString(this.getBytes());
            }
            return this.cachedHex;
        }
    
        @Override
        public String toBase64() {
            if(this.cachedBase64 == null) {
                this.cachedBase64 = Base64.encodeToString(this.getBytes());
            }
    
            return this.cachedBase64;
        }
    
        @Override
        public boolean isEmpty() {
            return this.bytes == null || this.bytes.length == 0;
        }
        @Override
        public String toString() {
            return this.toBase64();
        }
    
        @Override
        public int hashCode() {
            return this.bytes != null && this.bytes.length != 0? Arrays.hashCode(this.bytes):0;
        }
    
        @Override
        public boolean equals(Object o) {
            if(o == this) {
                return true;
            } else if(o instanceof ByteSource) {
                ByteSource bs = (ByteSource)o;
                return Arrays.equals(this.getBytes(), bs.getBytes());
            } else {
                return false;
            }
        }
    
        private static final class BytesHelper extends CodecSupport {
            private BytesHelper() {
            }
    
            public byte[] getBytes(File file) {
                return this.toBytes(file);
            }
    
            public byte[] getBytes(InputStream stream) {
                return this.toBytes(stream);
            }
        }
    
    }
    
  6. 登录接口编写:

@RequestMapping("/login")
@ResponseBody
public ResponseWrapper loginUser(@RequestBody User user) throws AuthenticationException {
    ResponseWrapper responseWrapper;
    boolean flags = authcService.login(user);
    if (flags){
        // 将会话信息存储在redis中,并在用户认证通过后将会话Id以token的形式返回给用户。用户请求受保护资源时带上这个token,我们根据token信息去redis中获取用户的权限信息,从而做访问控制。
        Serializable id = SecurityUtils.getSubject().getSession().getId();
        logger.debug("会话ID:"+id);
        responseWrapper=ResponseWrapper.markSuccess();
        responseWrapper.setExtra("token",id);
    }else {
        responseWrapper = ResponseWrapper.markNoData();
    }

    return responseWrapper;
}

在authService里的login方法里,通过用户名和密码生成UsernamePasswordToken然后调用subject.login(token)让shiro自己去处理:

@Service
public class AuthcServiceImpl implements AuthcService {
    @Override
    public boolean login(User user) throws AuthenticationException {
        if (user==null){
            return false;
        }

        if (user.getUsername()==null||"".equals(user.getUsername())){
            return false;
        }

        if (user.getPassword() == null || "".equals(user.getPassword())){
            return false;
        }
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken(user.getUsername(), user.getPassword());

        subject.login(token);
        return true;
    }
}

前端获取到token之后可以放到vuex里去,每次向后台发送请求可以在拦截器里将该token添加到Header里的“Authorization"里去

前端跨域问题:

image

vue.config.js文件里把上边的before: require('./mock/mock-server.js'),注释掉,并添加下边的代码

更改.dev.development文件里的VUE_APP_BASE_API

image

把utils文件夹里的request.js文件里的下边的code!=20000改为code!=200(这个看不同前端项目而定,如果这里不改,即使获取到了后台的代码,后台默认是200为正确的,这里前台判定是20000,前台就会报错,而不是返回后台的数据显示)
image

原文地址:https://www.cnblogs.com/RealGang/p/15433823.html