springboot环境下Shiro+Token+Redis安全认证方案

21 篇文章 0 订阅

springboot环境下Shiro+Token+Redis安全认证方案

什么是shiro

Apache Shiro 是一个强大灵活的开源安全框架,可以完全处理身份验证、授权、加密和会话管理。

Realm是Shiro的核心组建,也一样是两步走,认证和授权,在Realm中的表现为以下两个方法。

  • 认证:doGetAuthenticationInfo,核心作用判断登录信息是否正确
  • 授权:doGetAuthorizationInfo,核心作用是获取用户的权限字符串,用于后续的判断

Shiro过滤器

当Shiro被运用到web项目时,Shiro会自动创建一些默认的过滤器对客户端请求进行过滤。以下是 Shiro 提供的部分过滤器:

过滤器描述
anon表示可以匿名使用
authc表示需要认证(登录)才能使用
authcBasic表示httpBasic认证
perms当有多个参数时必须每个参数都通过才通过 perms[“user:add:”]
portport[8081] 跳转到schemal://serverName:8081?queryString
rest权限
roles角色
ssl表示安全的url请求
user表示必须存在用户,当登入操作时不做检查

springboot整合shiro

添加maven依赖

<!-- SECURITY begin -->
<dependency>
  <groupId>org.apache.shiro</groupId>
  <artifactId>shiro-spring</artifactId>
  <version>1.4.0</version>
</dependency>
<!-- SECURITY end -->

redis工具准备

@Bean
    @DependsOn("ConfigUtil")
    public JedisClusterClient getClient() {

        ml.ytooo.redis.RedisProperties.expireSeconds = redisProperties.getExpireSeconds();
        ml.ytooo.redis.RedisProperties.clusterNodes = redisProperties.getClusterNodes();
        ml.ytooo.redis.RedisProperties.connectionTimeout = redisProperties.getConnectionTimeout();
        ml.ytooo.redis.RedisProperties.soTimeout = redisProperties.getSoTimeout();
        ml.ytooo.redis.RedisProperties.maxAttempts = redisProperties.getMaxAttempts();

        if (StringUtils.isNotBlank(redisProperties.password)) {
            ml.ytooo.redis.RedisProperties.password = redisProperties.password;
        }else {
            ml.ytooo.redis.RedisProperties.password = null;
        }

        return JedisClusterClient.getInstance();
    }

@Data
@Component
@ConfigurationProperties(prefix = "redis.cache")
public class RedisProperties {

    private int expireSeconds;
    private String clusterNodes;
    private int  connectionTimeout;
    private String password;
    private int soTimeout;
    private int maxAttempts;
}```

### shiro自定义认证token
AuthenticationToken 用于收集用户提交的身份(如用户名)及凭据(如密码)。Shiro会调用CredentialsMatcher对象的doCredentialsMatch方法对AuthenticationInfo对象和AuthenticationToken进行匹配。匹配成功则表示主体(Subject)认证成功,否则表示认证失败。
Shiro 仅提供了一个可以直接使用的 UsernamePasswordToken,用于实现基于用户名/密码主体(Subject)身份认证。UsernamePasswordToken实现了 RememberMeAuthenticationTokenHostAuthenticationToken,可以实现“记住我”及“主机验证”的支持。
> 我们的业务逻辑是每次调用接口,不使用session存储登录状态,使用在head里面存token的方式,所以不使用session,并不需要用户密码认证。
**自定义token如下:**
```java
/**
 * Created by Youdmeng on 2020/6/24 0024.
 */
public class YtoooToken implements AuthenticationToken {
    private String token;
    public YtoooToken(String token) {
        this.token = token;
    }
    @Override
    public Object getPrincipal() {
        return token;
    }
    @Override
    public Object getCredentials() {
        return token;
    }
}

shiro自定义Realm

Realm是shiro的核心组件,主要处理两大功能:

  • 认证 我们接收filter传过来的token,并认证login操作的token
  • 授权 获取到登录用户信息,并取得用户的权限存入roles,以便后期对接口进行操作权限验证
@Slf4j
public class UserRealm extends AuthorizingRealm {
    @Autowired
    private JedisClusterClient jedis;
    /**
     * 大坑!,必须重写此方法,不然Shiro会报错
     */
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof YtoooToken;
    }
     /**
     * 授权
     *
     * @param principals
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        log.info("Shiro权限配置");
        String token = principals.toString();

        UserDetailVO userDetailVO = JSON.parseObject(jedis.get(token), UserDetailVO.class);

        Set<String> roles = new HashSet<>();
        roles.add(userDetailVO.getAuthType() + "");
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        info.setRoles(roles);
        return info;
    }
    /**
     * 认证
     *
     * @param token
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        log.info("Shiror认证");
        YtoooToken usToken = (YtoooToken) token;
        //获取用户的输入的账号.
        String sid = (String) usToken.getCredentials();
        if (StringUtils.isBlank(sid)) {
            return null;
        }
        log.info("sid: " + sid);
        return new SimpleAccount(sid, sid, "userRealm");
    }
}

shiro自定义拦截器

自定义shiro拦截器来控制指定请求的访问权限,并登录shiro以便认证

我们自定义shiro拦截器主要使用其中的两个方法:

  • isAccessAllowed() 判断是否可以登录到系统
  • onAccessDenied() 当isAccessAllowed()返回false时,登录被拒绝,进入此接口进行异常处理
/**
 * Created by Youdmeng on 2020/6/24 0024.
 */
@Slf4j
public class TokenFilter extends FormAuthenticationFilter {
    private String errorCode;
    private String errorMsg;
    private static JedisClusterClient jedis = JedisClusterClient.getInstance();
    /**
     * 如果在这里返回了false,请求onAccessDenied()
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {

        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String sid = httpServletRequest.getHeader("sid");
        if (StringUtils.isBlank(sid)) {
            this.errorCode = ResponseEnum.TOKEN_UNAVAILABLE.getCode();
            this.errorMsg = ResponseEnum.TOKEN_UNAVAILABLE.getMessage();
            return false;
        }
        log.info("sid: " + sid);
        UserDetailVO userInfo = null;
        try {
            userInfo = JSON.parseObject(jedis.get(sid), UserDetailVO.class);
        } catch (Exception e) {
            this.errorCode = ResponseEnum.TOKEN_EXPIRE.getCode();
            this.errorMsg = ResponseEnum.TOKEN_EXPIRE.getMessage();
            return false;
        }
        if (userInfo == null) {
            this.errorCode = ResponseEnum.TOKEN_EXPIRE.getCode();
            this.errorMsg = ResponseEnum.TOKEN_EXPIRE.getMessage();
            return false;
        }
        //刷新超时时间
        jedis.expire(sid, 30 * 60); //30分钟过期
        YtoooToken token = new YtoooToken(sid);
        // 提交给realm进行登入,如果错误他会抛出异常并被捕获
        getSubject(request, response).login(token);
        // 如果没有抛出异常则代表登入成功,返回true
        return true;
    }
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) {
        ResponseMessage result = Result.error(this.errorCode,this.errorMsg);
        String reponseJson = (new Gson()).toJson(result);
        response.setContentType("application/json; charset=utf-8");
        response.setCharacterEncoding("utf-8");
        ServletOutputStream outputStream = null;
        try {
            outputStream = response.getOutputStream();
            outputStream.write(reponseJson.getBytes());
        } catch (IOException e) {
            log.error("权限校验异常",e);
        } finally {
            if (outputStream != null){
                try {
                    outputStream.flush();
                    outputStream.close();
                } catch (IOException e) {
                    log.error("权限校验,关闭连接异常",e);
                }
            }
        }
        return false;
    }
}

配置ShiroConfig

springboot中,组件通过@Bean的方式交由spring统一管理,在这里需要配置 securityManager,shiroFilter,AuthorizationAttributeSourceAdvisor

注入realm


@Bean
public UserRealm userRealm() {
    UserRealm userRealm = new UserRealm();
    return userRealm;
}

注入securityManager

@Bean("securityManager")
public DefaultWebSecurityManager getManager(UserRealm realm) {
    DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
    // 使用自己的realm
    manager.setRealm(realm);
    /*
      * 关闭shiro自带的session,详情见文档
      * http://shiro.apache.org/session-management.html#SessionManagement-StatelessApplications%28Sessionless%29
      */
    DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
    DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
    defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
    subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
    manager.setSubjectDAO(subjectDAO);

    return manager;
}

注入shiroFilter

此处将自定义过滤器添加到shiro中,并配置具体哪些路径,执行shiro的那些过滤规则

@Bean("shiroFilter")
public ShiroFilterFactoryBean factory(DefaultWebSecurityManager securityManager) {
    ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();

    // 添加自己的过滤器并且取名为token
    Map<String, Filter> filterMap = new HashMap<>();
    filterMap.put("token", new TokenFilter());
    factoryBean.setFilters(filterMap);

    factoryBean.setSecurityManager(securityManager);
    /*
      * 自定义url规则
      * http://shiro.apache.org/web.html#urls-
      */
    Map<String, String> filterRuleMap = new HashMap<>();

    //swagger
    filterRuleMap.put("/swagger-ui.html", "anon");
    filterRuleMap.put("/**/*.js", "anon");
    filterRuleMap.put("/**/*.png", "anon");
    filterRuleMap.put("/**/*.ico", "anon");
    filterRuleMap.put("/**/*.css", "anon");
    filterRuleMap.put("/**/ui/**", "anon");
    filterRuleMap.put("/**/swagger-resources/**", "anon");
    filterRuleMap.put("/**/api-docs/**", "anon");
    //swagger
    //登录
    filterRuleMap.put("/login/login", "anon");
    filterRuleMap.put("/login/verifyCode", "anon");
    // 所有请求通过我们自己的JWT Filter
    filterRuleMap.put("/**", "token");
    factoryBean.setFilterChainDefinitionMap(filterRuleMap);
    return factoryBean;

配置DefaultAdvisorAutoProxyCreator

解决在@Controller注解的类的方法中加入@RequiresRole等shiro注解,会导致该方法无法映射请求,导致返回404。

@Bean
public static DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator(){
    DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator=new DefaultAdvisorAutoProxyCreator();
    /**
      * setUsePrefix(false)用于解决一个奇怪的bug。在引入spring aop的情况下。
      * 在@Controller注解的类的方法中加入@RequiresRole等shiro注解,会导致该方法无法映射请求,导致返回404。
      * 加入这项配置能解决这个bug
      */
    defaultAdvisorAutoProxyCreator.setUsePrefix(true);
    return defaultAdvisorAutoProxyCreator;
}

配置AuthorizationAttributeSourceAdvisor 使doGetAuthorizationInfo()Shiro权限配置生效

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

在接口中控制权限
使用RequiresRoles注解来配置该接口需要的权限
当配置logical=Logical.OR时,登录这配置的权限在1,2,3中任意一个,即可以成功访问接口

@ApiOperation("任务调度")
@PostMapping("/dispatch")
@RequiresRoles(value = { "1", "2", "3" }, logical = Logical.OR)
public ResponseMessage dispatch(@RequestBody @Valid DispatchVO dispatchVO) {

    log.info("任务调度开始 入参:" + JSON.toJSONString(dispatchVO));
    try {
        service.dispatch(dispatchVO);
        return Result.success(ResponseEnum.SUCCESS.getCode(), ResponseEnum.SUCCESS.getMessage());
    } catch (RuntimeException e) {
        log.error("任务调度失败", e);
        return Result.error(ResponseEnum.ERROR.getCode(), e.getMessage());
    } catch (Exception e) {
        log.error("任务调度失败", e);
        return Result.error(ResponseEnum.ERROR.getCode(), ResponseEnum.ERROR.getMessage());
    }
}

同一异常处理

配置全局异常处理

@ControllerAdvice
@Order(value=1)
public class ShiroExceptionAdvice {

    private static final Logger logger = LoggerFactory.getLogger(ShiroExceptionAdvice.class);
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    @ExceptionHandler({AuthenticationException.class, UnknownAccountException.class,
            UnauthenticatedException.class, IncorrectCredentialsException.class})
    @ResponseBody
    public ResponseMessage unauthorized(Exception exception) {
        logger.warn(exception.getMessage(), exception);
        logger.info("catch UnknownAccountException");
        return Result.error(ResponseEnum.NOT_AUTHORIZED.getCode(), ResponseEnum.NOT_AUTHORIZED.getMessage());
    }

    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    @ExceptionHandler(UnauthorizedException.class)
    @ResponseBody
    public ResponseMessage unauthorized1(UnauthorizedException exception) {
        logger.warn(exception.getMessage(), exception);
        return Result.error(ResponseEnum.NOT_AUTHORIZED.getCode(), ResponseEnum.NOT_AUTHORIZED.getMessage());
    }
}
  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

懒惰的coder

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值