• 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

  • 相关阅读:
    2-jenkins持续集成体系介绍
    第六天打卡
    第五天打卡(find用法)
    第五天打卡
    第四天打卡
    第三天打卡
    第一天:定个小目标,学习REDHAT,希望能去考下RHCE
    day12
    Python3的List操作和方法
    Python3字符串的操作
  • 原文地址:https://www.cnblogs.com/RealGang/p/15433823.html
Copyright © 2020-2023  润新知