Springboot下Shiro+Token使用redis做安全認(rèn)證方案

以前項目中權(quán)限認(rèn)證沒有使用安全框架寓娩,都是在自定義filter中判斷是否登錄以及用戶是否有操作權(quán)限的荔泳。
最近開了新項目裁着,搭架子時持钉,想到使用安全框架來解決認(rèn)證問題,spring security太過龐大第练,我們的項目不大阔馋,所以決定采用Shiro

什么是Shiro

Apache Shiro 是一個強(qiáng)大靈活的開源安全框架,可以完全處理身份驗證娇掏、授權(quán)呕寝、加密和會話管理。

Realm是Shiro的核心組建婴梧,也一樣是兩步走下梢,認(rèn)證和授權(quán),在Realm中的表現(xiàn)為以下兩個方法塞蹭。

  • 認(rèn)證:doGetAuthenticationInfo孽江,核心作用判斷登錄信息是否正確
  • 授權(quán):doGetAuthorizationInfo,核心作用是獲取用戶的權(quán)限字符串番电,用于后續(xù)的判斷

Shiro過濾器

當(dāng) Shiro 被運用到 web 項目時岗屏,Shiro 會自動創(chuàng)建一些默認(rèn)的過濾器對客戶端請求進(jìn)行過濾。以下是 Shiro 提供的部分過濾器:

過濾器 描述
anon 表示可以匿名使用
authc 表示需要認(rèn)證(登錄)才能使用
authcBasic 表示httpBasic認(rèn)證
perms 當(dāng)有多個參數(shù)時必須每個參數(shù)都通過才通過 perms[“user:add:”]
port port[8081] 跳轉(zhuǎn)到schemal://serverName:8081?queryString
rest 權(quán)限
roles 角色
ssl 表示安全的url請求
user 表示必須存在用戶钧舌,當(dāng)?shù)侨氩僮鲿r不做檢查

為什么選擇shiro

  • 簡單性担汤,Shiro 在使用上較 Spring Security 更簡單,更容易理解洼冻。
  • 靈活性崭歧,Shiro 可運行在 Web、EJB撞牢、IoC率碾、Google App Engine 等任何應(yīng)用環(huán)境,卻不依賴這些環(huán)境屋彪。而 Spring Security 只能與 Spring 一起集成使用所宰。
  • 可插拔,Shiro 干凈的 API 和設(shè)計模式使它可以方便地與許多的其它框架和應(yīng)用進(jìn)行集成畜挥。Shiro 可以與諸如 Spring仔粥、Grails、Wicket蟹但、Tapestry躯泰、Mule、Apache Camel华糖、Vaadin 這類第三方框架無縫集成麦向。Spring Security 在這方面就顯得有些捉衿見肘。

spring boot整合shiro

添加maven依賴

在項目中引入shiro非常簡單客叉,我們只需要引入 shiro-pring 就可以了

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

shiro自定義認(rèn)證token

AuthenticationToken 用于收集用戶提交的身份(如用戶名)及憑據(jù)(如密碼)诵竭。Shiro會調(diào)用CredentialsMatcher對象的doCredentialsMatch方法對AuthenticationInfo對象和AuthenticationToken進(jìn)行匹配话告。匹配成功則表示主體(Subject)認(rèn)證成功,否則表示認(rèn)證失敗卵慰。

Shiro 僅提供了一個可以直接使用的 UsernamePasswordToken沙郭,用于實現(xiàn)基于用戶名/密碼主體(Subject)身份認(rèn)證。UsernamePasswordToken實現(xiàn)了 RememberMeAuthenticationToken 和 HostAuthenticationToken呵燕,可以實現(xiàn)“記住我”及“主機(jī)驗證”的支持棠绘。

我們的業(yè)務(wù)邏輯是每次調(diào)用接口件相,不使用session存儲登錄狀態(tài)再扭,使用在head里面存token的方式,所以不使用session夜矗,并不需要用戶密碼認(rèn)證泛范。

自定義token如下:

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

  • 認(rèn)證 我們接收filter傳過來的token紊撕,并認(rèn)證login操作的token
  • 授權(quán) 獲取到登錄用戶信息罢荡,并取得用戶的權(quán)限存入roles,以便后期對接口進(jìn)行操作權(quán)限驗證
@Slf4j
public class UserRealm extends AuthorizingRealm {
    @Autowired
    private JedisClusterClient jedis;
    /**
     * 大坑对扶!区赵,必須重寫此方法,不然Shiro會報錯
     */
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof YtoooToken;
    }
     /**
     * 授權(quán)
     *
     * @param principals
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        log.info("Shiro權(quán)限配置");
        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;
    }
    /**
     * 認(rèn)證
     *
     * @param token
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        log.info("Shiror認(rèn)證");
        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攔截器來控制指定請求的訪問權(quán)限浪南,并登錄shiro以便認(rèn)證

我們自定義shiro攔截器主要使用其中的兩個方法:

  • isAccessAllowed() 判斷是否可以登錄到系統(tǒng)
  • onAccessDenied() 當(dāng)isAccessAllowed()返回false時笼才,登錄被拒絕,進(jìn)入此接口進(jìn)行異常處理
/**
 * 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進(jìn)行登入骡送,如果錯誤他會拋出異常并被捕獲
        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("權(quán)限校驗異常",e);
        } finally {
            if (outputStream != null){
                try {
                    outputStream.flush();
                    outputStream.close();
                } catch (IOException e) {
                    log.error("權(quán)限校驗,關(guān)閉連接異常",e);
                }
            }
        }
        return false;
    }
}

配置ShiroConfig

springboot中絮记,組件通過@Bean的方式交由spring統(tǒng)一管理摔踱,在這里需要配置 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);
    /*
      * 關(guān)閉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中,并配置具體哪些路徑撰洗,執(zhí)行shiro的那些過濾規(guī)則

@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規(guī)則
      * 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注解篮愉,會導(dǎo)致該方法無法映射請求,導(dǎo)致返回404了赵。

@Bean
public static DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator(){
    DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator=new DefaultAdvisorAutoProxyCreator();
    /**
      * setUsePrefix(false)用于解決一個奇怪的bug潜支。在引入spring aop的情況下。
      * 在@Controller注解的類的方法中加入@RequiresRole等shiro注解柿汛,會導(dǎo)致該方法無法映射請求冗酿,導(dǎo)致返回404埠对。
      * 加入這項配置能解決這個bug
      */
    defaultAdvisorAutoProxyCreator.setUsePrefix(true);
    return defaultAdvisorAutoProxyCreator;
}

配置 AuthorizationAttributeSourceAdvisor 使doGetAuthorizationInfo()Shiro權(quán)限配置生效

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

在接口中控制權(quán)限

使用RequiresRoles注解來配置該接口需要的權(quán)限

當(dāng)配置logical = Logical.OR時,登錄這配置的權(quán)限在1,2,3中任意一個裁替,既可以成功訪問接口

@ApiOperation("任務(wù)調(diào)度")
@PostMapping("/dispatch")
@RequiresRoles(value = { "1", "2", "3" }, logical = Logical.OR)
public ResponseMessage dispatch(@RequestBody @Valid DispatchVO dispatchVO) {

    log.info("任務(wù)調(diào)度開始 入?yún)?" + JSON.toJSONString(dispatchVO));
    try {
        service.dispatch(dispatchVO);
        return Result.success(ResponseEnum.SUCCESS.getCode(), ResponseEnum.SUCCESS.getMessage());
    } catch (RuntimeException e) {
        log.error("任務(wù)調(diào)度失敗", e);
        return Result.error(ResponseEnum.ERROR.getCode(), e.getMessage());
    } catch (Exception e) {
        log.error("任務(wù)調(diào)度失敗", e);
        return Result.error(ResponseEnum.ERROR.getCode(), ResponseEnum.ERROR.getMessage());
    }
}

統(tǒng)一的異常處理

配置全局異常處理

@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>

收工





更多好玩好看的內(nèi)容项玛,歡迎到我的博客交流,共同進(jìn)步????????WaterMin


?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末弱判,一起剝皮案震驚了整個濱河市襟沮,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌昌腰,老刑警劉巖开伏,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異遭商,居然都是意外死亡固灵,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進(jìn)店門劫流,熙熙樓的掌柜王于貴愁眉苦臉地迎上來巫玻,“玉大人,你說我怎么就攤上這事祠汇∪猿樱” “怎么了?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵可很,是天一觀的道長诗力。 經(jīng)常有香客問我,道長根穷,這世上最難降的妖魔是什么姜骡? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮屿良,結(jié)果婚禮上圈澈,老公的妹妹穿的比我還像新娘。我一直安慰自己尘惧,他們只是感情好康栈,可當(dāng)我...
    茶點故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著喷橙,像睡著了一般啥么。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上贰逾,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天悬荣,我揣著相機(jī)與錄音,去河邊找鬼疙剑。 笑死氯迂,一個胖子當(dāng)著我的面吹牛践叠,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播嚼蚀,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼禁灼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了轿曙?” 一聲冷哼從身側(cè)響起弄捕,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎导帝,沒想到半個月后守谓,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡舟扎,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年分飞,在試婚紗的時候發(fā)現(xiàn)自己被綠了悴务。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片睹限。...
    茶點故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖讯檐,靈堂內(nèi)的尸體忽然破棺而出羡疗,到底是詐尸還是另有隱情,我是刑警寧澤别洪,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布叨恨,位于F島的核電站,受9級特大地震影響挖垛,放射性物質(zhì)發(fā)生泄漏痒钝。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一痢毒、第九天 我趴在偏房一處隱蔽的房頂上張望送矩。 院中可真熱鬧,春花似錦哪替、人聲如沸栋荸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽晌块。三九已至,卻和暖如春帅霜,著一層夾襖步出監(jiān)牢的瞬間匆背,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工身冀, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留钝尸,地道東北人蜂大。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像蝶怔,于是被迫代替她去往敵國和親奶浦。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,786評論 2 345