切換語言為:簡體

SpringBoot整合Shiro+JWT框架,實現無狀態登入與許可權認證

  • 爱糖宝
  • 2024-09-29
  • 2048
  • 0
  • 0

摘要:本文主要描述如何快速基於SpringBoot 2.5.X版本整合Shiro+JWT框架,讓大家快速實現無狀態登陸和介面許可權認證主體框架,具體業務細節未實現,大家按照實際專案補充。

背景

  • 為什麼要使用Shiro

隨大流吧,雖然自己也可以基於自定義註解+攔截器實現和Shiro一樣的功能,但是爲了適用於業界的規範,所以整合這個大家都能看得懂,而且Shiro也相對簡單。

  • 為什麼要用Jwt

傳統的session模式越來越少,而且大多數系統都是微服務多客戶端的,所以無狀態的登陸更符合現階段的業務架構。

開始

本案例基於SpringBoot 2.5.X + Shiro 1.8 + hutool的Jwt

pom.xml

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>2.5.4</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
            <version>2.5.4</version>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.24</version>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring-boot-web-starter</artifactId>
            <version>1.8.0</version>
        </dependency>
    </dependencies>

ResponseMessage返回訊息體

@Data
public class ResponseMessage<T> {

    private Boolean success = Boolean.TRUE;

    private String code;

    private String message;

    private T data;

    public static <T> ResponseMessage<T> success(T data){
        ResponseMessage<T> responseMessage = new ResponseMessage<>();
        responseMessage.setCode("200");
        responseMessage.setMessage("操作成功");
        responseMessage.setData(data);
        return responseMessage;
    }

    public static <T> ResponseMessage<T> fail(String message){
        ResponseMessage<T> responseMessage = new ResponseMessage<>();
        responseMessage.setSuccess(Boolean.FALSE);
        responseMessage.setCode("500");
        responseMessage.setMessage(message);
        return responseMessage;
    }

}

GlobalExceptionHandler全域性異常處理

@Slf4j
@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(value = Exception.class)
    @ResponseBody
    public ResponseMessage<String> handleAllExceptions(Exception ex, WebRequest request) {
        // 處理異常
        log.error("業務異常",ex);
        return ResponseMessage.fail(ex.getMessage());
    }

}

JwtUtils工具類

public class JwtUtils {

    private static final byte[] KEY = "ABADEXU".getBytes();

    public static String createToken(Map<String, Object> payload){
        return JWTUtil.createToken(payload, JwtUtils.KEY);
    }

    public static JWT parseToken(String token){
        return JWTUtil.parseToken(token);
    }

    public static Boolean verify(String token){
        return JWTUtil.verify(token, JwtUtils.KEY);
    }
}

JwtToken 認證dto

@Data
public class JwtToken implements AuthenticationToken {

    /** JWT 認證串 */
    private String jwt;

    public JwtToken(String jwt) {
        this.jwt = jwt;
    }

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

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

}

JwtFilter 許可權認證過濾器

攔截請求介面的

@Slf4j
public class JwtFilter extends AccessControlFilter {


    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
        HttpServletRequest httpServletRequest   = (HttpServletRequest) request;
        String jwt                              = httpServletRequest.getHeader("Authorization");
        if(StrUtil.isNotBlank(jwt)){
            getSubject(request, response).login(new JwtToken(jwt));
            return true;
        }
        return false;
    }

    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        throw new RuntimeException("身份驗證異常");
    }

}

JwtRealm 授權領域

授權流程JwtFilter#isAccessAllowed -> JwtRealm#supports -> JwtRealm#doGetAuthenticationInfo -> JwtRealm#doGetAuthorizationInfo

@Slf4j
public class JwtRealm extends AuthorizingRealm {

    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JwtToken;
    }

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        log.info("驗證jwt token 許可權");
        String jwt = principalCollection.getPrimaryPrincipal().toString();
        // 這裏一般就從redis中拿使用者的許可權資訊,案例就直接寫死了
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        // 設定角色
        simpleAuthorizationInfo.addRole("admin");
        simpleAuthorizationInfo.addRole("user");
        // 設定許可權
        simpleAuthorizationInfo.addStringPermission("user:add");
        return simpleAuthorizationInfo;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        log.info("驗證jwt token 有效性");
        String jwt = authenticationToken.getPrincipal().toString();
        // 從redis查詢jwt token是否還存在,是否有效
        if(!Boolean.TRUE.equals(JwtUtils.verify(jwt))){
            throw new RuntimeException("jwt token 失效");
        }
        JWT parseToken = JwtUtils.parseToken(jwt);
        Object expiryTime = parseToken.getPayload("expiryTime");
        // 驗證token是否過期
        return new SimpleAuthenticationInfo(jwt, jwt, this.getClass().getName());
    }
}

ShiroConfig 配置

@Configuration
public class ShiroConfig {

    @Bean
    public JwtRealm jwtRealm(){
        return new JwtRealm();
    }

    @Bean
    public DefaultWebSecurityManager defaultWebSecurityManager() {
        DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
        defaultWebSecurityManager.setRealm(jwtRealm());
        /*
         * 關閉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);
        defaultWebSecurityManager.setSubjectDAO(subjectDAO);
        return defaultWebSecurityManager;
    }

    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
        shiroFilter.setSecurityManager(defaultWebSecurityManager());
        // 未授權跳轉
        shiroFilter.setUnauthorizedUrl("/unauthorized");
        Map<String, Filter> filterMap = new HashMap<>();
        //
        filterMap.put("jwt", new JwtFilter());
        //
        shiroFilter.setFilters(filterMap);

        Map<String, String> filterRuleMap = new LinkedHashMap<>();
        // 匿名訪問
        filterRuleMap.put("/error", "anon");
        filterRuleMap.put("/login", "anon");
        filterRuleMap.put("/logout", "anon");
        filterRuleMap.put("/unauthorized", "anon");
        // 登入並具有 admin 角色
        // filterRuleMap.put("/index/admin", "authc,roles[admin]");
        // filterRuleMap.put("/index/admin", "jwt,roles[admin]");
        // 透過jwt校驗,需登入才能訪問(自行實現邏輯)
        filterRuleMap.put("/**", "jwt");
        //
        shiroFilter.setFilterChainDefinitionMap(filterRuleMap);
        //
        return shiroFilter;
    }

    /**
     * 開啟Shiro的註解(如@RequiresRoles,@RequiresPermissions)
     * @return
     */
    @Bean
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        advisorAutoProxyCreator.setUsePrefix(true);
        return advisorAutoProxyCreator;
    }

}

測試相關

LoginController 登陸介面
@RestController
public class LoginController {

    @GetMapping("login")
    public Object login(String username){
        Map<String, Object> payload = new HashMap<>();
        payload.put("username", username);
        // 設定30分鐘後過期
        payload.put("expiryTime", DateUtil.date().offset(DateField.MINUTE, 30));
        return JwtUtils.createToken(payload);
    }

}

UserController 許可權驗證介面
@Slf4j
@RestController
@RequestMapping(value = "user")
public class UserController {

    @RequiresPermissions(value = {"user:view"})
    @GetMapping(value = "page")
    public Object page(){
        log.info("page");
        return "SUCCESS";
    }

    @RequiresPermissions(value = {"user:add"})
    @GetMapping(value = "add")
    public Object add(){
        log.info("add");
        return "SUCCESS";
    }

}

0則評論

您的電子郵件等資訊不會被公開,以下所有項目均必填

OK! You can skip this field.