【Spring boot】Shiro + JWT 搭建無狀態 RESTful 架構

技術棧:Spring boot + Shiro +JWT 

先說一下 Spring security 和 Shiro ,從這兩者選擇的時候最後還是選擇了Shiro,原因是Spring security 偏重,適合大型企業項目,而且現在用Shiro的也不少。網上這兩個的對比文章還是很多的,這裏就不贅述。

Shiro默認實現的是session形式,也就是有狀態的。我們要改變一些東西,來實現無狀態的RESTful 架構~

1.  pom.xml

只需要加入這兩個包就可以

<!--  shiro  -->
<dependency>
   <groupId>org.apache.shiro</groupId>
   <artifactId>shiro-spring</artifactId>
   <version>1.4.0</version>
</dependency>
<!--  JWT  -->
<dependency>
   <groupId>com.auth0</groupId>
   <artifactId>java-jwt</artifactId>
   <version>3.8.0</version>
</dependency>

2. JWT配置

1. JWTToken.java 實體類

public class JWTToken implements AuthenticationToken {

    private String token;

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

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

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

 2. JWTUtil.java 來實現JWT的token解析等工具類

public class JWTUtil {

    // 過期時間一小時
    private static final long EXPIRE_TIME = 60*60*1000;
    private static final Logger PLOG = LoggerFactory.getLogger(JWTUtil.class);
    /**
     * 校驗token是否正確
     * @param token TOKEN
     * @param secret 用戶的密碼
     * @return boolean 
     */
    public static boolean verify(String token, String username, String secret) {
        try {
            Algorithm algorithm = Algorithm.HMAC256(secret);
            JWTVerifier verifier = JWT.require(algorithm)
                    .withClaim("username", username)
                    .build();
            DecodedJWT jwt = verifier.verify(token);
            return true;
        } catch (Exception e) {
            PLOG.error("JWT >> " + e);
            return false;
        }
    }

    /**
     * 無需解密直接獲得token中的用戶名
     * @return token中包含的用戶名
     */
    public static String getUsername(String token) {
        try {
            DecodedJWT jwt = JWT.decode(token);
            return jwt.getClaim("username").asString();
        } catch (JWTDecodeException e) {
            PLOG.error("JWT >> " + e);
            return null;
        }
    }

    /**
     * 生成簽名,並設置過期時間
     * @param username 用戶名
     * @param secret 用戶的密碼
     * @return token
     */
    public static String sign(String username, String secret) {
        try {
            Date date = new Date(System.currentTimeMillis()+EXPIRE_TIME);
            Algorithm algorithm = Algorithm.HMAC256(secret);
            // 附帶username信息
            return JWT.create()
                    .withClaim("username", username)
                    .withExpiresAt(date)
                    .sign(algorithm);
        } catch (Exception e) {
            PLOG.error("JWT >> " + e);
            return null;
        }
    }
}

3. JWTFilter.java 我們要加一個我們自己的Shiro過濾器,並配置在Shiro

public class JWTFilter extends BasicHttpAuthenticationFilter {
    private static final Logger PLOG = LoggerFactory.getLogger(JWTFilter.class);
    /**
     * 判斷用戶是否想要登入。
     * 檢測header裏面是否包含Authorization字段即可
     */
    @Override
    protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
        HttpServletRequest req = (HttpServletRequest) request;
        String authorization = req.getHeader("Authorization");
        return true;
    }
    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String authorization = httpServletRequest.getHeader("Authorization");
        JWTToken token = new JWTToken(authorization);
        // 提交給realm進行登入,如果錯誤他會拋出異常並被捕獲
        getSubject(request, response).login(token);
        // 如果沒有拋出異常則代表登入成功,返回true
        return true;
    }
    /**
     * 這裏控制通過與否
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        if (isLoginAttempt(request, response)) {
            try {
                executeLogin(request, response);
            } catch (Exception e) {
                PLOG.error("JWT >> " + e);
                responseError(request, response);
                return false;
            }
        }
        return true;
    }
    /**
     * 將非法請求跳轉到 /ign/error
     */
    private void responseError(ServletRequest req, ServletResponse resp) {
        try {
            HttpServletResponse httpServletResponse = (HttpServletResponse) resp;
            httpServletResponse.sendRedirect("/user/ign/error");
        } catch (IOException e) {
            PLOG.error("JWT >> " + e);
        }
    }
}

3. Shiro配置

CustomRealm.java 

doGetAuthenticationInfo中拋出異常來進行身份判定

public class CustomRealm extends AuthorizingRealm {
    private UserMapper userMapper;

    private static final Logger PLOG = LoggerFactory.getLogger(CustomRealm.class);

    @Autowired
    private void setUserMapper(UserMapper userMapper) {
        this.userMapper = userMapper;
    }

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

    /**
     * 獲取身份驗證信息
     * Shiro中,最終是通過 Realm 來獲取應用程序中的用戶、角色及權限信息的。
     *
     * @param authenticationToken 用戶身份信息 token
     * @return 返回封裝了用戶信息的 AuthenticationInfo 實例
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        PLOG.info("Shiro >> 身份認證");
        String token = (String) authenticationToken.getCredentials();
        if (token == null) {
            throw new AuthenticationException("token invalid");
        }
        String username = JWTUtil.getUsername(token);
        if (username == null) {
            throw new AuthenticationException("token invalid");
        }
        // 從數據庫獲取對應用戶名密碼的用戶
        String password = userMapper.getPasswordByUsername(username);
        if (null == password) {
           throw new AuthenticationException("User didn't existed!");
        }
        if (!JWTUtil.verify(token, username, password)) {
            throw new AuthenticationException("Username or password error");
        }
        return new SimpleAuthenticationInfo(token, token, getName());
    }

    /**
     * 獲取授權信息
     *
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        PLOG.info("Shiro >> 權限認證");
        String username = JWTUtil.getUsername(principalCollection.toString());
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        //獲得該用戶角色
        Integer power = userMapper.getPowerByUsername(username);
        Set<String> set = new HashSet<>();
        if (power == 100) {
            set.add("admin");
        }
        set.add("user");
        info.setRoles(set);
        return info;
    }
}

 

ShiroConfig.java

從這裏加上我們上文寫過的JWTFilter 以及配置一下權限控制

@Configuration
public class ShiroConfig {
    private static final Logger PLOG = LoggerFactory.getLogger(ShiroConfig.class);
    @Bean
    public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        // 必須設置 SecurityManager
        shiroFilterFactoryBean.setSecurityManager(securityManager);

        // 自定義過濾器
        Map<String, Filter> filterMap = new HashMap<>();
        filterMap.put("jwt", new JWTFilter());
        shiroFilterFactoryBean.setFilters(filterMap);

        shiroFilterFactoryBean.setLoginUrl("/user/ign/notLogin");
        shiroFilterFactoryBean.setUnauthorizedUrl("/user/ign/notRole");

        // 設置攔截器
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();

        // 開放登錄、未登錄等映射
        filterChainDefinitionMap.put("/user/ign/**", "anon");

        // 攔截接口
        filterChainDefinitionMap.put("/user/**", "jwt");

        // 其餘接口一律攔截
        // filterChainDefinitionMap.put("/**", "authc");

        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);

        PLOG.info("Shiro >> Shiro攔截器工廠類注入成功");
        return shiroFilterFactoryBean;
    }

    /**
     * 注入 securityManager
     */
    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 設置 realm.
        securityManager.setRealm(customRealm());

        // 關閉 shiro 自帶的session
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        securityManager.setSubjectDAO(subjectDAO);

        return securityManager;
    }

    /**
     * 自定義身份認證 realm;
     * <p>
     * 必須寫這個類,並加上 @Bean 註解,目的是注入 CustomRealm,
     * 否則會影響 CustomRealm類 中其他類的依賴注入
     */
    @Bean
    public CustomRealm customRealm() {
        return new CustomRealm();
    }

    /**
     * 下面的代碼是添加註解支持
     */
    @Bean
    @DependsOn("lifecycleBeanPostProcessor")
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        // 強制使用cglib,防止重複代理和可能引起代理出錯的問題
        // https://zhuanlan.zhihu.com/p/29161098
        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
        return defaultAdvisorAutoProxyCreator;
    }

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

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

4. 登錄

登錄的Controller 中,生成token並返給前端使用

    @PostMapping(value = "/ign/login")
    public ServerResponse<String> login( @RequestBody User user) {
        User sourceUser = userService.getUserByUsername(user.getUsername());
        String md5Pass = Analysis.knoveMD5(user.getPassword());
        if (sourceUser.getPassword().equals(md5Pass)) {
            PLOG.info("UserController >> login · 獲取Token");
            return ServerResponse.createBySuccess(JWTUtil.sign(user.getUsername(), md5Pass));
        }
        return  ServerResponse.createByError("登錄失敗!");
    }

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章