SpringBoot+Shiro+Ehcache+Redis整合

SpringBoot+Shiro+Ehcache

公司项目重构, 用到了 SpringBoot+SpringMVC+Mybatis+Shiro+Redis等技术, 这篇博客用于记录 SpringBoot整合 Shiro的一些问题. 这篇博客很多地方引用了 https://blog.csdn.net/weixin_42236404/article/details/89319359#comments_12032156

本项目前后端分离, 使用了 Redis作为登录 Token缓存, 并设置过期时间, 下面上代码:

1: pom文件相关依赖

		<properties>
			<java.version>1.8</java.version>
			<shiro.version>1.3.2</shiro.version>
		</properties>
		<!-- SpringBoot整合Spring Data Redis -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-redis</artifactId>
		</dependency>
		<!-- redis的驱动包:jedis -->
		<dependency>
			<groupId>redis.clients</groupId>
			<artifactId>jedis</artifactId>
		</dependency>

		<!--shiro核心-->
		<dependency>
			<groupId>org.apache.shiro</groupId>
			<artifactId>shiro-core</artifactId>
			<version>${shiro.version}</version>
		</dependency>
		<!--shiro的Web模块-->
		<dependency>
			<groupId>org.apache.shiro</groupId>
			<artifactId>shiro-web</artifactId>
			<version>${shiro.version}</version>
		</dependency>
		<!--shiro和Spring集成-->
		<dependency>
			<groupId>org.apache.shiro</groupId>
			<artifactId>shiro-spring</artifactId>
			<version>${shiro.version}</version>
		</dependency>
		<!--shiro底层使用的ehcache缓存-->
		<dependency>
			<groupId>org.apache.shiro</groupId>
			<artifactId>shiro-ehcache</artifactId>
			<version>${shiro.version}</version>
		</dependency>
		<dependency>
			<groupId>net.sf.ehcache</groupId>
			<artifactId>ehcache-core</artifactId>
			<version>2.6.8</version>
		</dependency>
		

2: 登录入口

	@ApiOperation(value = "用户登录", httpMethod = "POST")
    @PostMapping(value = "/login")
    @ResponseBody
    @ApiImplicitParams({
            @ApiImplicitParam(name = "username", value = "账号", required = true),
            @ApiImplicitParam(name = "password", value = "密码", required = true)
    })
    public JsonResult login(@RequestParam("username") String username, @RequestParam("password") String password, HttpServletResponse response) throws Exception {
        // 根据用户名查询用户
        Qxczry qxczry = qxczryService.getCzryById(username);
        if (qxczry == null || !qxczry.getQxczryzhxx().getPassword().equals(WebEncrypt.encode(password))) {
            return new JsonResult("400", "账号或密码错误");
        } else {
            String token = qxczryService.createToken(qxczry.getCzrydm());
            CookieUtil.addCookie(response, "USER_LOGIN_TOKEN", token, 30 * 60);
            return new JsonResult("200", "登陆成功");
        }
    }
	@ApiOperation(value = "用户登出", httpMethod = "POST", notes = "Parameter或 Header中必须携带 token")
    @ResponseBody
    @ApiImplicitParams({
            @ApiImplicitParam(name = "token", value = "登录令牌", required = true)
    })
    @PostMapping("/logout")
    public JsonResult logout(HttpServletRequest httpServletRequest) {
        String token = TokenUtil.getRequestToken(httpServletRequest);
        qxczryService.logout(token);
        return new JsonResult("200", "您已安全退出系统");
    }

WebEncrypt类作用是加密, 此处不贴出

3: 编写 Service方法, 生成一个 Token, 并保存到 Redis中

	@Override
    public Qxczry getCzryById(String czrydm) {
        return qxczryMapper.getCzryById(czrydm);
    }

    @Override
    public String createToken(String czrydm) {
        // 生成一个token
        String token = TokenGenerator.generateValue();
        // 将 token保存到 Redis中, 并设置存活时间
        redisTemplate.opsForValue().set(token, czrydm, 30L, TimeUnit.MINUTES);
        // 并将 token返回
        return token;
    }

    @Override
    public void logout(String token) {
        String userUUID = redisTemplate.opsForValue().get(token);
        redisTemplate.opsForValue().set(token, userUUID, 1L, TimeUnit.MILLISECONDS);
    }

ps: 登出方法写的很 LOW, 别介意, 这是初版 - -

4: 编写 ShiroConfig类

@Configuration
public class ShiroConfig {

    /**
     * 缓存管理器
     * @return
     */
    @Bean
    public EhCacheManager ehCacheManager(){
        EhCacheManager cacheManager = new EhCacheManager();
        cacheManager.setCacheManagerConfigFile("classpath:ehcache.xml");
        return cacheManager;
    }


    /**
     * aop代理
     * @return
     */
    @Bean
    @DependsOn("lifecycleBeanPostProcessor")
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
        return defaultAdvisorAutoProxyCreator;
    }


    /**
     * 自定义realm
     * @return
     */
    @Bean
    public AuthRealm authRealm() {
        AuthRealm authRealm = new AuthRealm();
        authRealm.setCacheManager(ehCacheManager());
        return authRealm;
    }


    @Bean("securityManager")
    public SecurityManager securityManager(AuthRealm authRealm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(authRealm);
        securityManager.setRememberMeManager(null);
        securityManager.setCacheManager(ehCacheManager());
        return securityManager;
    }


    @Bean("shiroFilter")
    public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
        shiroFilter.setSecurityManager(securityManager);
        //auth过滤
        Map<String, Filter> filters = new HashMap<>();
        filters.put("auth", new AuthFilter());
        shiroFilter.setFilters(filters);
        Map<String, String> filterMap = new LinkedHashMap<>();
        // anno匿名访问  auth验证
        filterMap.put("/webjars/**", "anon");
        filterMap.put("/druid/**", "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("/**", "auth");
        shiroFilter.setFilterChainDefinitionMap(filterMap);

        return shiroFilter;
    }


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


    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }

}

5: 实现自定义的 AuthenticationToken

public class AuthToken extends UsernamePasswordToken {
    private String token;

    public AuthToken(String token) {
        this.token = token;
    }

    @Override
    public Object getPrincipal() {
        return token;
    }

    @Override
    public Object getCredentials() {
        return token;
    }
}

6: 编写自己的 Realm

@Component("AuthRealm")
public class AuthRealm extends AuthorizingRealm {

    @Resource
    @Lazy
    private RoleMapper roleMapper;
    @Resource
    @Lazy
    private PermissionMapper permissionMapper;
    @Resource
    @Lazy
    private QxczryMapper qxczryMapper;
    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
     * 权限认证,即登录过后,每个身份不一定,对应的所能看的页面也不一样
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        // 获得当前登录的用户对象
        Qxczry qxCzry = (Qxczry) principalCollection.getPrimaryPrincipal();

        // 查询该用户的所有角色
        List<String> roleSns = roleMapper.selectRoleByCzryDm(qxCzry.getCzrydm());
        // 查询该用户的所有权限
        List<String> expressions = permissionMapper.selectGnmkzyByCzryDm(qxCzry.getCzrydm());
        // 将角色和权限添加到授权信息对象
        info.addRoles(roleSns);
        info.addStringPermissions(expressions);
        return info;
    }

/**
     * 身份认证,即登录通过账号和密码验证登陆人的身份信息
     * @param token
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {String accessToken = (String) token.getPrincipal();
        //1. 根据accessToken,查询用户信息
        String tokenEntity = redisTemplate.opsForValue().get(accessToken);
        //2. token失效
        if (tokenEntity == null) {
            throw new IncorrectCredentialsException("token失效,请重新登录");
        }
        //3. 调用数据库的方法, 从数据库中查询 username 对应的用户记录
        Qxczry qxczry = qxczryMapper.getCzryById(tokenEntity);
        //4. 若用户不存在, 则可以抛出 UnknownAccountException 异常
        if (qxczry == null) {
            throw new UnknownAccountException("用户不存在!");
        }
        //5. 根据用户的情况, 来构建 AuthenticationInfo 对象并返回. 通常使用的实现类为: SimpleAuthenticationInfo
        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(qxczry, accessToken, this.getName());
        return info;
    }
}

需添加 @Lazy注解, 否则 Service缓存注解、事务注解不生效

7: 实现自定义 AuthenticatingFilter

public class AuthFilter extends AuthenticatingFilter {
    /**
     * 生成自定义token
     *
     * @param request
     * @param response
     * @return
     * @throws Exception
     */
    @Override
    protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {
        //获取请求token
        String token = TokenUtil.getRequestToken((HttpServletRequest) request);
        return new AuthToken(token);
    }

    /**
     * 步骤1.所有请求全部拒绝访问
     *
     * @param request
     * @param response
     * @param mappedValue
     * @return
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        if (((HttpServletRequest) request).getMethod().equals(RequestMethod.OPTIONS.name())) {
            return true;
        }
        return false;
    }

    /**
     * 步骤2,拒绝访问的请求,会调用onAccessDenied方法,onAccessDenied方法先获取 token,再调用executeLogin方法
     *
     * @param request
     * @param response
     * @return
     * @throws Exception
     */
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        //获取请求token,如果token不存在,直接返回
        String token = TokenUtil.getRequestToken((HttpServletRequest) request);
        if (StringUtils.isBlank(token)) {
            HttpServletResponse httpResponse = (HttpServletResponse) response;
            httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
            httpResponse.setHeader("Access-Control-Allow-Origin", HttpContextUtil.getOrigin());
            httpResponse.setCharacterEncoding("UTF-8");
            Map<String, Object> result = new HashMap<>();
            result.put("status", 400);
            result.put("msg", "请先登录");
            String json = JSON.toJSONString(result);
            httpResponse.getWriter().print(json);
            return false;
        }
        return executeLogin(request, response);
    }

    /**
     * token失效时候调用
     */
    @Override
    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        httpResponse.setContentType("application/json;charset=utf-8");
        httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
        httpResponse.setHeader("Access-Control-Allow-Origin", HttpContextUtil.getOrigin());
        httpResponse.setCharacterEncoding("UTF-8");
        try {
            //处理登录失败的异常
            Throwable throwable = e.getCause() == null ? e : e.getCause();
            System.out.println(e);
            Map<String, Object> result = new HashMap<>();
            result.put("status", 400);
            result.put("msg", "登录凭证已失效,请重新登录");

            String json = JSON.toJSONString(result);
            httpResponse.getWriter().print(json);
        } catch (IOException e1) {
        }
        return false;
    }
}

补充:
ecachche.xml

<?xml version="1.0" encoding="UTF-8"?>
<ehcache updateCheck="false" dynamicConfig="false">
    <diskStore path="java.io.tmpdir"/>

    <cache name="users"
           timeToLiveSeconds="300"
           maxEntriesLocalHeap="1000"/>

    <!--
        name:缓存名称。
        maxElementsInMemory:缓存最大个数。
        eternal:对象是否永久有效,一但设置了,timeout将不起作用。
        timeToIdleSeconds:设置对象在失效前的允许闲置时间(单位:秒)。仅当eternal=false对象不是永久有效时使用,可选属性,默认值是0,也就是可闲置时间无穷大。
        timeToLiveSeconds:设置对象在失效前允许存活时间(单位:秒)。最大时间介于创建时间和失效时间之间。仅当eternal=false对象不是永久有效时使用,默认是0.,也就是对象存活时间无穷大。
        overflowToDisk:当内存中对象数量达到maxElementsInMemory时,Ehcache将会对象写到磁盘中。
        diskSpoolBufferSizeMB:这个参数设置DiskStore(磁盘缓存)的缓存区大小。默认是30MB。每个Cache都应该有自己的一个缓冲区。
        maxElementsOnDisk:硬盘最大缓存个数。
        diskPersistent:是否缓存虚拟机重启期数据 Whether the disk store persists between restarts of the Virtual Machine. The default value is false.
        diskExpiryThreadIntervalSeconds:磁盘失效线程运行时间间隔,默认是120秒。
        memoryStoreEvictionPolicy:当达到maxElementsInMemory限制时,Ehcache将会根据指定的策略去清理内存。默认策略是LRU(最近最少使用)。你可以设置为FIFO(先进先出)或是LFU(较少使用)。
        clearOnFlush:内存数量最大时是否清除。
    -->
    <defaultCache name="defaultCache"
                  maxElementsInMemory="10000"
                  eternal="false"
                  timeToIdleSeconds="120"
                  timeToLiveSeconds="120"
                  overflowToDisk="false"
                  maxElementsOnDisk="100000"
                  diskPersistent="false"
                  diskExpiryThreadIntervalSeconds="120"
                  memoryStoreEvictionPolicy="LRU"/>
</ehcache>
public class HttpContextUtil {
    public static HttpServletRequest getHttpServletRequest() {
        return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
    }

    public static String getDomain() {
        HttpServletRequest request = getHttpServletRequest();
        StringBuffer url = request.getRequestURL();
        return url.delete(url.length() - request.getRequestURI().length(), url.length()).toString();
    }

    public static String getOrigin() {
        HttpServletRequest request = getHttpServletRequest();
        return request.getHeader("Origin");
    }
}
public class CookieUtil {

    /**
     * 添加 Cookie
     * @param response 响应对象
     * @param name
     * @param value
     * @param timeout  生命存活周期
     */
    public static void addCookie(HttpServletResponse response, String name, String value, int timeout) {
        // 将token封装cookie中, 通过response对象返回到浏览器中
        Cookie cookie = new Cookie(name, value);
        // 设置作用域
        cookie.setPath("/");
        // 设置生命周期
        cookie.setMaxAge(timeout);
        response.addCookie(cookie);
    }

    /**
     * 获取token
     * @param request token
     */
    public static String getToken(HttpServletRequest request, HttpServletResponse response) {
        Cookie[] cookies = request.getCookies();
        if(cookies != null){
            for (Cookie cookie : cookies) {
                if(cookie.getName().equals("USER_LOGIN_TOKEN")){
                    cookie.setMaxAge(30 * 60);
                    // response.addCookie(cookie);
                    return cookie.getValue();
                }
            }
        }
        return null;
    }
}

AuthRealm类权限认证会加载该角色锁拥有的权限和贴有 @RequiresPermissions注解的类做比对, 也就是说只有贴了此注解才被 Shiro所管理

	@ApiOperation(value = "重新加载所有权限", httpMethod = "POST")
    @PostMapping("/reloadPermission")
    @RequiresPermissions(value = {"权限管理:重新加载所有权限", "permission:reloadPermission"}, logical = Logical.OR)
    public JsonResult<Boolean> reloadPermission(){
        JsonResult<Boolean> result = new JsonResult<>();
        try {
            permissionService.reload();
            result.setCode("200");
            result.setData(true);
        } catch (Exception e) {
            e.printStackTrace();
            result.setCode("500");
            result.setMsg("加载失败");
            result.setData(false);
        }
        return result;
    }

以下为流程图:
流程图
项目尚未完成, 不过 Shiro基本功能已实现, 以上代码有以下细节未处理:
1. 登录后用户操作不能刷新客户端 Cookie的存活时间
2. 无权限异常提示未处理

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值