Springboot下Shiro+Token使用redis作安全認證方案

之前項目中權限認證沒有使用安全框架,都是在自定義filter中判斷是否登陸以及用戶是否有操做權限的。
最近開了新項目,搭架子時,想到使用安全框架來解決認證問題,spring security太過龐大,咱們的項目不大,因此決定採用Shiro

什麼是Shiro

Apache Shiro 是一個強大靈活的開源安全框架,能夠徹底處理身份驗證、受權、加密和會話管理。css

Realm是Shiro的核心組建,也同樣是兩步走,認證和受權,在Realm中的表現爲如下兩個方法。html

  • 認證:doGetAuthenticationInfo,核心做用判斷登陸信息是否正確
  • 受權:doGetAuthorizationInfo,核心做用是獲取用戶的權限字符串,用於後續的判斷

Shiro過濾器

當 Shiro 被運用到 web 項目時,Shiro 會自動建立一些默認的過濾器對客戶端請求進行過濾。如下是 Shiro 提供的部分過濾器:java

過濾器 描述
anon 表示能夠匿名使用
authc 表示須要認證(登陸)才能使用
authcBasic 表示httpBasic認證
perms 當有多個參數時必須每一個參數都經過才經過 perms[「user:add:」]
port port[8081] 跳轉到schemal://serverName:8081?queryString
rest 權限
roles 角色
ssl 表示安全的url請求
user 表示必須存在用戶,當登入操做時不作檢查

爲何選擇shiro

  • 簡單性,Shiro 在使用上較 Spring Security 更簡單,更容易理解。
  • 靈活性,Shiro 可運行在 Web、EJB、IoC、Google App Engine 等任何應用環境,卻不依賴這些環境。而 Spring Security 只能與 Spring 一塊兒集成使用。
  • 可插拔,Shiro 乾淨的 API 和設計模式使它能夠方便地與許多的其它框架和應用進行集成。Shiro 能夠與諸如 Spring、Grails、Wicket、Tapestry、Mule、Apache Camel、Vaadin 這類第三方框架無縫集成。Spring Security 在這方面就顯得有些捉衿見肘。

spring boot整合shiro

添加maven依賴

在項目中引入shiro很是簡單,咱們只須要引入 shiro-pring 就能夠了web

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

shiro自定義認證token

AuthenticationToken 用於收集用戶提交的身份(如用戶名)及憑據(如密碼)。Shiro會調用CredentialsMatcher對象的doCredentialsMatch方法對AuthenticationInfo對象和AuthenticationToken進行匹配。匹配成功則表示主體(Subject)認證成功,不然表示認證失敗。redis

Shiro 僅提供了一個能夠直接使用的 UsernamePasswordToken,用於實現基於用戶名/密碼主體(Subject)身份認證。UsernamePasswordToken實現了 RememberMeAuthenticationToken 和 HostAuthenticationToken,能夠實現「記住我」及「主機驗證」的支持。spring

咱們的業務邏輯是每次調用接口,不使用session存儲登陸狀態,使用在head裏面存token的方式,因此不使用session,並不須要用戶密碼認證。

自定義token以下:apache

/**
 * 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的核心組件,主要處理兩大功能:json

  • 認證 咱們接收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攔截器主要使用其中的兩個方法:api

  • 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());
    }
}

上面使用的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;
}

依賴工具集:

<dependency>
  <groupId>ml.ytooo</groupId>
  <artifactId>ytooo-util</artifactId>
  <version>3.7.0</version>
</dependency>

收工





更多好玩好看的內容,歡迎到個人博客交流,共同進步        WaterMin

相關文章
相關標籤/搜索