SrpingBoot-Shrio 整合 JWT (7)-不夠完善

SrpingBoot-Shrio 整合 JWT (2019.12.23)

JSON Web Token(JWT)是一個非常輕巧的規範。這個規範允許我們使用JWT在用戶和服務器之間傳遞安全可靠的信息。

使用基於 Token 的身份驗證方法,在服務端不需要存儲用戶的登錄記錄。

  1. 客戶端使用用戶名跟密碼請求登錄
  2. 服務端收到請求,去驗證用戶名與密碼
  3. 驗證成功後,服務端會簽發一個 Token,再把這個 Token 發送給客戶端
  4. 客戶端收到 Token 以後可以把它存儲起來,比如放在 Cookie 裏
  5. 客戶端每次向服務端請求資源的時候需要帶着服務端簽發的 Token
  6. 服務端收到請求,然後去驗證客戶端請求裏面帶着的 Token,如果驗證成功,就向客戶端返回請求的數據

一個JWT實際上就是一個字符串,它由三部分組成: 頭部、載荷與簽名。

1. 引入JWT依賴

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <parent>
    <groupId>spring-boot-demo-shiro-base</groupId>
    <artifactId>spring-boot-demo-shiro-base</artifactId>
    <version>1.0-SNAPSHOT</version>
  </parent>

  <artifactId>spring-boot-demo-shiro6</artifactId>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <java.version>1.8</java.version>
    <mybatis.version>1.3.2</mybatis.version>
    <mysql.version>8.0.12</mysql.version>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
      <groupId>org.mybatis.spring.boot</groupId>
      <artifactId>mybatis-spring-boot-starter</artifactId>
      <version>${mybatis.version}</version>
    </dependency>
    <!--使用阿里巴巴的德魯伊作爲數據源-->
    <dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>druid-spring-boot-starter</artifactId>
      <version>1.1.10</version>
    </dependency>
  <!--mysql數據庫-->
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <!-- thymeleaf -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
    <!--Shiro 依賴-->
    <dependency>
      <groupId>org.apache.shiro</groupId>
      <artifactId>shiro-spring-boot-starter</artifactId>
      <version>1.4.0-RC2</version>
    </dependency>
    <!--jjwt 依賴-->
    <dependency>
      <groupId>io.jsonwebtoken</groupId>
      <artifactId>jjwt</artifactId>
      <version>0.7.0</version>
    </dependency>
    <!-- shiro-redis -->
    <dependency>
      <groupId>org.crazycake</groupId>
      <artifactId>shiro-redis</artifactId>
      <version>2.4.2.1-RELEASE</version>
      <exclusions>
        <exclusion>
          <artifactId>shiro-core</artifactId>
          <groupId>org.apache.shiro</groupId>
        </exclusion>
      </exclusions>
    </dependency>
	<!--html頁面使用shiro標籤依賴-->
    <dependency>
      <groupId>com.github.theborakompanioni</groupId>
      <artifactId>thymeleaf-extras-shiro</artifactId>
      <version>2.0.0</version>
    </dependency>
    <!-- 對象池,使用redis時必須引入 -->
    <dependency>
      <groupId>org.apache.commons</groupId>
      <artifactId>commons-pool2</artifactId>
    </dependency>
    <!--lombok-->
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <optional>true</optional>
    </dependency>
  </dependencies>
</project>

2.application.yml 加上jwt配置信息

server:
  port: 8080
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/shiro?serverTimezone=UTC
    username: root
    password: root
    type: com.alibaba.druid.pool.DruidDataSource
    #數據源其他配置
    druid:
      initial-size: 5
      min-idle: 5
      max-active: 20
      max-wait: 60000
      time-between-eviction-runs-millis: 60000
      min-evictable-idle-time-millis: 300000
      validation-query: SELECT 1 FROM DUAL
      test-while-idle: true
      test-on-borrow: false
      test-on-return: false
      pool-prepared-statements: true
  redis:
    host: localhost
    # 連接超時時間(記得添加單位,Duration)
    timeout: 10000ms
    lettuce:
      pool:
        # 連接池最大連接數(使用負值表示沒有限制) 默認 8
        max-active: 8
        # 連接池最大阻塞等待時間(使用負值表示沒有限制) 默認 -1
        max-wait: -1ms
        # 連接池中的最大空閒連接 默認 8
        max-idle: 8
        # 連接池中的最小空閒連接 默認 0
        min-idle: 0
#jwt 配置
jwt:
  config:
    key: zhihao  #加密密匙
    # token有效期,單位秒
    jwtTimeOut: 3600
    # 後端免認證接口 url
    anonUrl: /login,
mybatis:
  mapper-locations: mapper/*.xml
  type-aliases-package: com.zhihao.entity

3.創建jwtUtil.java工具類,進行簽發和解析token

@Component
@ConfigurationProperties(prefix = "jwt.config")
public class JwtUtil {

    @Value("${jwt.config.key}")
    private String key;

    private long jwtTimeout;//一個小時

    private String anonUrl;

    public String getKey() {
        return key;
    }

    public void setKey(String key) {
        this.key = key;
    }

    public long getJwtTimeout() {
        return jwtTimeout;
    }

    public void setJwtTimeout(long jwtTimeout) {
        this.jwtTimeout = jwtTimeout;
    }

    public String getAnonUrl() {
        return anonUrl;
    }

    public void setAnonUrl(String anonUrl) {
        this.anonUrl = anonUrl;
    }

    /**
     * 生成JWT
     *
     * @param id      用戶id
     * @param subject 用戶名
     * @return java.lang.String
     */
    public String createJWT(String id, String subject) {
        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);
        JwtBuilder builder = Jwts.builder()
                .setId(id) //id
                .setSubject(subject) //主題
                .setIssuedAt(now) //簽發時間
                .signWith(SignatureAlgorithm.HS256, key); //加密
        //超時大於0 設置token超時
        if (jwtTimeout > 0) {
            //轉換成超時毫秒
            long timeout = nowMillis + (jwtTimeout * 1000);
            builder.setExpiration(new Date(timeout));
        }
        return builder.compact();
    }

    /**
     * 解析JWT
     *
     * @param jwtStr
     * @return
     */
    public Claims parseJWT(String jwtStr){
        return Jwts.parser()
                .setSigningKey(key)
                .parseClaimsJws(jwtStr)
                .getBody();

    }
}

4.自定義自己的過濾器JWTFilter

我們使用的是 shiro 默認的權限攔截 Filter,而因爲JWT的整合,我們需要自定義自己的過濾器 JWTFilter,JWTFilter 繼承了 BasicHttpAuthenticationFilter,並部分原方法進行了重寫。

  1. 檢驗請求頭是否帶有 token ((HttpServletRequest) request).getHeader("Token") != null
  2. 如果帶有 token,執行 shiro 的 login() 方法,將 token 提交到 Realm 中進行檢驗;如果沒有 token,說明當前狀態爲遊客狀態(或者其他一些不需要進行認證的接口)
public class JWTFilter extends BasicHttpAuthenticationFilter {

    private Logger log = LoggerFactory.getLogger(this.getClass());

    private static final String TOKEN = "Token";

    private AntPathMatcher pathMatcher = new AntPathMatcher();

    private Map errorMap;


    @SneakyThrows
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws UnauthorizedException {
        WebApplicationContext context = WebApplicationContextUtils.getWebApplicationContext(request.getServletContext());
        JwtUtil JwtUtil = context.getBean(JwtUtil.class);
        //判斷是否是登錄請求
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String[] split = StringUtils.split(JwtUtil.getAnonUrl(), ",");
        for (String url : split) {
            //如果是後端免認證接口 直接放行
            if (pathMatcher.match(url,httpServletRequest.getRequestURI())){
                return true;
            }
        }
        //進入executeLogin方法判斷請求的請求頭是否帶上 "Token"
        if (isLoginAttempt(request, response)) {
            //如果存在,則進入 executeLogin 方法執行登入,檢查 token 是否正確
            return executeLogin(request, response);
        }else {
            this.tokenError(response,"token爲空");
        }
        return false;
    }

    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws IOException {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String token = httpServletRequest.getHeader(TOKEN);
        try {
            // 提交給realm進行登入,如果錯誤他會拋出異常並被捕獲
            getSubject(request, response).login(new JWTToken(token));
            // 如果沒有拋出異常則代表登入成功,返回true
            return true;
        } catch (Exception e) {
            this.tokenError(response, "token認證失敗");
            return false;
        }
    }

    @Override
    protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
        HttpServletRequest req = (HttpServletRequest) request;
        String token = req.getHeader(TOKEN);
        return token != null;
    }

    /**
     * token問題響應
     *
     * @param response
     * @param msg
     * @return void
     * @author: zhihao
     * @date: 2019/12/24
     * {@link #}
     */
    private void tokenError(ServletResponse response,String msg) throws IOException {
        errorMap = new LinkedHashMap();
        errorMap.put("code", "error");
        errorMap.put("msg", msg);
        //響應token爲空
        response.setContentType("application/json;charset=UTF-8");
        response.setCharacterEncoding("UTF-8");
        response.resetBuffer(); //清空第一次流響應的內容
        //轉成json格式
        ObjectMapper object = new ObjectMapper();
        String asString = object.writeValueAsString(errorMap);
        response.getWriter().println(asString);
    }

    /**
     * 對跨域提供支持
     */
    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
        httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
        httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
        // 跨域時會首先發送一個 option請求,這裏我們給 option請求直接返回正常狀態
        if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
            httpServletResponse.setStatus(HttpStatus.OK.value());
            return false;
        }
        return super.preHandle(request, response);
    }
}

5, JWTToken.java

@Data
public class JWTToken implements AuthenticationToken {

    private String token;

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

    public JWTToken() {
    }

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

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

}

6. 配置ShiroConfig

@Configuration
public class ShiroConfig {

    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        // 設置securityManager
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        // 在 Shiro過濾器鏈上加入 JWTFilter
        LinkedHashMap<String, Filter> filters = new LinkedHashMap<>();
        filters.put("jwt", new JWTFilter());
        shiroFilterFactoryBean.setFilters(filters);

        LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
		//所有url都必須認證通過jwt過濾器纔可以訪問
        filterChainDefinitionMap.put("/**", "jwt");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }

    @Bean
    public DefaultWebSecurityManager securityManager() {
        // 配置SecurityManager,並注入shiroRealm
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(shiroRealm());
        //設置緩存管理器 省略..
        return securityManager;
    }

    @Bean
    public ShiroRealm shiroRealm() {
        // 配置Realm,需自己實現
        ShiroRealm shiroRealm = new ShiroRealm();
        return shiroRealm;
    }
    /**
     * 開啓shiro認證註解
     *
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }
}

7. 配置自己實現的ShiroRealm

public class ShiroRealm extends AuthorizingRealm {

    /**
     *  支持自定義認證令牌
     *
     * @param token
     * @return boolean
     * @author: zhihao
     * @date: 2019/12/24
     */
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JWTToken;
    }

    @Autowired
    private UserService userService;

    @Autowired
    private RoleService roleService;

    @Autowired
    private PermissionService permissionService;

    @Autowired
    private JwtUtil jwtUtil;

    /**
     * 授權模塊>>>>獲取用戶角色和權限
     *
     * @param principal
     * @return org.apache.shiro.authz.AuthorizationInfo
     * @author: zhihao
     * @date: 2019/12/13
     * {@link #}
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principal) {
       	User user = (User) SecurityUtils.getSubject().getPrincipal();
        String username = user.getUsername();
        //創建授權對象進行封裝角色和權限信息進去進行返回 注意不是SimpleAuthenticationInfo
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        //獲取用戶角色集
        List<Role> roleList = roleService.findRoleByUserName(username);
        Set<String> roleSet = new HashSet<>();
        for (Role role : roleList) {
            roleSet.add(role.getMemo());
        }
        System.out.println("用戶擁有的角色>>>"+roleSet);
        //添加角色進角色授權
        info.setRoles(roleSet);
        List<Permission> permissionList = permissionService.findPermissionByUserName(username);
        Set<String> permissionSet = new HashSet<>();
        for (Permission permission : permissionList) {
            permissionSet.add(permission.getName());
        }
        System.out.println("用戶擁有的權限>>>"+permissionSet);
        //添加權限進權限授權
        info.setStringPermissions(permissionSet);
        return info;
    }

    /**
     * 用戶認證
     *
     * @param authenticationToken 身份認證 token
     * @return AuthenticationInfo 身份認證信息
     * @throws AuthenticationException 認證相關異常
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) {

        // 這裏的 token是從 JWTFilter 的 executeLogin 方法傳遞過來的
        String token = (String) authenticationToken.getCredentials();
        String username = null;
        try {
            username = jwtUtil.parseJWT(token).getSubject();
        } catch (Exception e) {
            //拋出token認證失敗
            throw new AuthenticationException("token認證失敗");
        }
        // 通過用戶名到數據庫查詢用戶信息
        User user = userService.findUserByName(username);
        if (user == null) {
            throw new UnknownAccountException("用戶不存在!");
        }
        if (user.getStatus().equals("0")) {
            throw new LockedAccountException("賬號已被鎖定,請聯繫管理員!");
        }
        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, token, getName());
        return info;
    }
}

7. 配置權限不足異常處理器

/**
 * @Author: zhihao
 * @Date: 2019/12/24 12:57
 * @Description: 登錄相關異常處理
 * @Versions 1.0
 **/
@ControllerAdvice
@Order(value = Ordered.HIGHEST_PRECEDENCE)
public class loginExceptionHandler {
    private Logger log = LoggerFactory.getLogger(this.getClass());

    private Map exceptionMap;
    /**
     * 攔截權限不足異常,並響應
     *
     * @param e
     * @return java.util.Map
     * @author: zhihao
     * @date: 2019/12/24
     */
    @ExceptionHandler(value = UnauthorizedException.class)
    @ResponseBody
    public Map unauthorizedException(UnauthorizedException e){
        exceptionMap = new LinkedHashMap();
        exceptionMap.put("msg", e.getMessage());
        log.error(e.getMessage());
        return exceptionMap;
    }
}

8.登錄接口,校驗成功簽發token

 /**
     * 登錄免認證  登錄成功簽發token
     *
     * @param username 用戶名
     * @param password 密碼
     * @return java.util.Map 簡陋的結果包裝
     * @author: zhihao
     * @date: 2019/12/12
     */
    @PostMapping("/login")
    public Map login(@RequestParam("username") String username,@RequestParam("password") String password){
        resultMap = new LinkedHashMap<>();
        // 密碼加密
        String md5 = new SimpleHash("MD5", password, username, 1024).toString();
        User user = userService.findUserByName(username);
        if(user != null && md5.equals(user.getPassword())){
            resultMap.put("code", "success");
            resultMap.put("token", jwtUtil.createJWT(user.getId(), user.getUsername()));
            return resultMap;
        }
        resultMap.put("code","error");
        resultMap.put("msg","用戶不存在或者密碼錯誤");
        return resultMap;
    }

9. 進行測試

項目代碼:

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