xmtrock
发布于 2021-07-26 / 282 阅读
0

微服务中,Shiro+Redis完成会话管理和验证

前置

<!--shiro与spring整合 -->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>1.3.2</version>
</dependency>
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-core</artifactId>
    <version>1.3.2</version>
</dependency>
<!--shiro与redis整合实现sessionDao -->
<dependency>
    <groupId>org.crazycake</groupId>
    <artifactId>shiro-redis</artifactId>
    <version>3.0.0</version>
</dependency>
redis:
  host: 47.115.203.188
  port: 6604
  password: flowerpower

多对多查询什么的之类的就省略了吧

父工程

这个baseController是给子controller拿来继承

public class BaseController {
    public HttpServletRequest request;
    public HttpServletResponse response;
    protected String companyId;
    protected String companyName;

    //shiro方式
    @ModelAttribute
    public void setResAndReq(HttpServletRequest request, HttpServletResponse response) {
        this.request = request;
        this.response = response;

        //获取session中的安全数据
        Subject subject = SecurityUtils.getSubject();
        //获取安全数据集合
        PrincipalCollection principals = subject.getPrincipals();
        if(principals!=null && !principals.isEmpty()){
            //获取安全数据
            ProfileResult result = (ProfileResult) principals.getPrimaryPrincipal();
            this.companyId = result.getCompanyId();
            this.companyName = result.getCompany();
        }
    }
}

配置公共的realm,这个realm的作用是用于授权的。因为授权信息是从session里存着,每个微服务都是同一个获取用法

public class IhrmRealm extends AuthorizingRealm {
    /**
     * 搞这个主要是免得realm多时重名冲突了
     */
    public void setName(String name) {
        super.setName("ihrmRealm"); //customRealm
    }

    @Override//授权
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {

        //获取安全数据,注意不是user,因为认证传的不是user
        ProfileResult result = (ProfileResult) principals.getPrimaryPrincipal();
        //获取权限信息
        Set<String> apisPerms = (Set<String>) result.getRoles().get("apis");
        //构造返回权限数据
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        info.setStringPermissions(apisPerms);
        return info;
    }

    @Override//认证
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        /**
         * 这里不需要写,应该交由子类来认证,因为每个微服务都需要不同的认证信息。
         * 我们只要的是公共的权限信息,由上面授权部分来完成
         */
        return null;
    }
}

然后是配置自定义的sessionManager,用于拿出header里的token(sessionId)

public class CustomSessionManager extends DefaultWebSessionManager {
    /**
     * 指定sessionId 的获取方式
     * 头信息中具有sessionid
     * 请求头:Authoirzation: sessionId
     */
    @Override
    protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
        //获取请求头的数据
        String id = WebUtils.toHttp(request).getHeader("Authorization");

        if (StringUtils.isEmpty(id)) {
            //没有就生成一个
            return super.getSessionId(request, response);
        } else {
            id = id.replaceAll("Bearer ", "");
            //返回sessionId注意开头有Bearer
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, "header");//哪里获取
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);//id是啥
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);//要不要验证
            return id;
        }
    }
}

最好还是给一个公共的认证错误或未登录的controller

@RestController//公共错误跳转
public class ErrorController {
    @RequestMapping("/autherror")
    public Result autherror(int code){
        return code==1?Result.FAIL(ResultCode.UNAUTHENTICATED):Result.FAIL(ResultCode.UNAUTHORISE);
    }
}

子工程

子dependency里面,继承父的realm,重写认证方法

//子类不用重写realm名
public class UserRealm extends IhrmRealm {
    @Resource
    private UserService userService;
    @Resource
    private PermissionService permissionService;

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        //获取到用户的手机号密码
        UsernamePasswordToken uptoken = (UsernamePasswordToken) token;
        String mobile = uptoken.getUsername();
        String password = new String(uptoken.getPassword());

        //根据手机号查询用户
        User user = userService.resultWithAll(new QueryWrapper<User>().lambda().eq(User::getMobile, mobile));
        //判断用户存在与否,密码一致与否
        if (user != null && user.getPassword().equals(password)) {
            //安全数据构造与返回(用户基本数据和权限信息就可,否则数据太多了.profileResult)
            ProfileResult profileResult = null;
            //根据不同的用户级别来获取用户权限
            //如果是普通用户(user),则只有特定用户自己的权限
            //如果是coAdmin管理员则给出全体权限
            //平台全体管理员呢???
            if (user.getLevel().equals("user")) {
                profileResult = new ProfileResult(user);
            } else {
                Map<String, Object> map = new HashMap<>();
                if ("coAdmin".equals(user.getLevel()))
                    // 1仅查询企业的权限,企业权限应当一视同仁。0是隐藏的。
                    map.put("enVisible", 1);
                List<Permission> all = permissionService.findAll(map);
                profileResult = new ProfileResult(user, all);
            }
            SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(profileResult, user.getPassword(), this.getName());
            return info;
        }

        //null会返回异常,表示用户名密码不匹配
        return null;
    }
}

最重点是这个自定义的shiro配置

@Configuration
public class ShiroConfiguration {
    //创建Realm
    @Bean
    public IhrmRealm getRealm() { return new UserRealm(); }

    //创建安全管理器
    @Bean(name = "securityManager")
    public SecurityManager getSecurityManager(IhrmRealm customRealm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(customRealm);

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

        return securityManager;
    }

    //配置过滤器工厂
    @Bean(name = "shiroFilter")
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
        //web中,shiro进行权限控制全部是通过一组过滤器集合进行控制
        //创建过滤器工厂
        ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
        //设置安全管理器
        factoryBean.setSecurityManager(securityManager);
        //通用配置(跳转登录页面,未授权跳转的页面)
        factoryBean.setLoginUrl("/autherror?code=1");
        factoryBean.setUnauthorizedUrl("/autherror?code=2");
        /**
         * 设置过滤器集合,有顺序的map
         * key=url拦截地址,value=过滤器类型
         */
        Map<String, String> filterMap = new LinkedHashMap<>();
        filterMap.put("/sys/login","anon");//此地址允许匿名
        filterMap.put("/autherror","anon");//此地址允许匿名
        filterMap.put("/**", "authc");//必须认证

        factoryBean.setFilterChainDefinitionMap(filterMap);
        return factoryBean;
    }

    //开启注解支持
    @Bean(name = "authorizationAttributeSourceAdvisor")
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }
    @Bean(name = "defaultAdvisorAutoProxyCreator")
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);//cglib方式
        return defaultAdvisorAutoProxyCreator;
    }


    //redis的控制器,操作控制器(jar里有,不用自己写的)
    public RedisManager redisManager() {
        RedisManager redisManager = new RedisManager();
        redisManager.setHost("47.115.203.188");
        redisManager.setPort(6604);
        redisManager.setPassword("flowerpower");
        return redisManager;
    }
    //缓存管理器
    public RedisCacheManager redisCacheManager() {
        RedisCacheManager cacheManager = new RedisCacheManager();
        cacheManager.setRedisManager(redisManager());
        return cacheManager;
    }

    //sessionDao
    public RedisSessionDAO redisSessionDAO() {
        RedisSessionDAO sessionDAO = new RedisSessionDAO();
        sessionDAO.setRedisManager(redisManager());
        return sessionDAO;
    }
    //自定义的会话管理器了
    public DefaultWebSessionManager redisResessionManager() {
        CustomSessionManager sessionManager = new CustomSessionManager();
        sessionManager.setSessionDAO(redisSessionDAO());
        //禁用cookie,禁用url重写(太难看?)
        sessionManager.setSessionIdCookieEnabled(false);
        sessionManager.setSessionIdUrlRewritingEnabled(false);
        return sessionManager;
    }
}

Controller里的登录、验证、权限等等。

@ApiOperation("Shiro用户登录")
@PostMapping("/login")
public Result login(@RequestBody Map<String, String> map) {
    String mobile = map.get("mobile");
    String password = map.get("password");

    try {
        //构造登录令牌
        password = new Md5Hash(password, mobile, 1020).toString();
        UsernamePasswordToken uptoken = new UsernamePasswordToken(mobile, password);

        //获取subject
        Subject subject = SecurityUtils.getSubject();

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

        //获取sessionId
        String sessionId = (String) subject.getSession().getId();

        //构造返回结果
        return Result.SUCCESS(sessionId);
    } catch (AuthenticationException e) {
        return Result.FAIL(ResultCode.MOBILEORPASSWORDERROR);
    }
}

验证

@ApiOperation("Shiro登录成功返回用户信息")
@PostMapping("/profile")
public Result profile() {
    Subject subject = SecurityUtils.getSubject();
    //subject获取session中的安全数据
    PrincipalCollection principals = subject.getPrincipals();
    ProfileResult result = (ProfileResult) principals.getPrimaryPrincipal();
    return Result.SUCCESS(result);
}

权限验证

@ApiOperation("根据ID删除User")
@RequiresPermissions("API-USER-DELETE")
@DeleteMapping(value = "/user/{id}", name = "API-USER-DELETE")
public Result deleteById(@PathVariable String id) {
    boolean b = userService.deleteById(id);
    return b ? Result.SUCCESS() : Result.FAIL();
}

https://gitee.com/happylayga/ihrm