Przeglądaj źródła

rest授权校验使用jwt,并联合服务端验证

zhouchenglin 6 lat temu
rodzic
commit
bf97eade6a

+ 26 - 0
pom.xml

@@ -44,9 +44,30 @@
         <swagger-version>2.9.2</swagger-version>
         <servlet-version>3.1.0</servlet-version>
         <kaptcha-version>2.3.2</kaptcha-version>
+        <jwt-version>0.9.0</jwt-version>
     </properties>
 
     <dependencies>
+        <!-- 单元测试 start -->
+        <dependency>
+            <groupId>org.testng</groupId>
+            <artifactId>testng</artifactId>
+            <version>6.9.10</version>
+            <scope>test</scope>
+            <exclusions>
+                <exclusion>
+                    <groupId>junit</groupId>
+                    <artifactId>junit</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-all</artifactId>
+            <version>1.10.19</version>
+            <scope>test</scope>
+        </dependency>
+        <!-- 单元测试 end -->
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-test</artifactId>
@@ -192,6 +213,11 @@
             <version>${servlet-version}</version>
             <scope>provided</scope>
         </dependency>
+        <dependency>
+            <groupId>io.jsonwebtoken</groupId>
+            <artifactId>jjwt</artifactId>
+            <version>${jwt-version}</version>
+        </dependency>
     </dependencies>
 
     <build>

+ 29 - 5
src/main/java/net/chenlin/dp/common/support/interceptor/RestApiInterceptor.java

@@ -1,15 +1,15 @@
 package net.chenlin.dp.common.support.interceptor;
 
+import io.jsonwebtoken.JwtException;
 import net.chenlin.dp.common.annotation.RestAnon;
 import net.chenlin.dp.common.constant.RestApiConstant;
-import net.chenlin.dp.common.utils.JSONUtils;
-import net.chenlin.dp.common.utils.SpringContextUtils;
-import net.chenlin.dp.common.utils.TokenUtils;
-import net.chenlin.dp.common.utils.WebUtils;
+import net.chenlin.dp.common.utils.*;
 import net.chenlin.dp.modules.sys.entity.SysUserEntity;
 import net.chenlin.dp.modules.sys.entity.SysUserTokenEntity;
 import net.chenlin.dp.modules.sys.service.SysUserService;
 import org.apache.commons.lang.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.springframework.context.annotation.DependsOn;
 import org.springframework.web.method.HandlerMethod;
 import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
@@ -25,8 +25,12 @@ import javax.servlet.http.HttpServletResponse;
 @DependsOn("springContextUtils")
 public class RestApiInterceptor extends HandlerInterceptorAdapter {
 
+    private static final Logger log = LoggerFactory.getLogger(RestApiInterceptor.class);
+
     private SysUserService userService = (SysUserService) SpringContextUtils.getBean("sysUserService");
 
+    private JwtUtils jwtUtils = SpringContextUtils.getBean("jwtUtils", JwtUtils.class);
+
     /**
      * 拦截
      * @param request
@@ -68,23 +72,43 @@ public class RestApiInterceptor extends HandlerInterceptorAdapter {
             WebUtils.write(response, JSONUtils.beanToJson(RestApiConstant.TokenErrorEnum.TOKEN_NOT_FOUND.getResp()));
             return false;
         }
+        try {
+            boolean flag = jwtUtils.isExpred(token);
+            if (flag) {
+                WebUtils.write(response, JSONUtils.beanToJson(RestApiConstant.TokenErrorEnum.TOKEN_EXPIRED.getResp()));
+                return false;
+            }
+        } catch (JwtException e) {
+            log.info("token解析异常:{}", token);
+            return false;
+        }
+
         // token校验
-        SysUserTokenEntity sysUserTokenEntity = userService.getUserTokenByToken(token);
+        SysUserTokenEntity sysUserTokenEntity = userService.getUserTokenByToken(jwtUtils.getMd5Key(token));
         if (sysUserTokenEntity == null) {
             WebUtils.write(response, JSONUtils.beanToJson(RestApiConstant.TokenErrorEnum.TOKEN_INVALID.getResp()));
             return false;
         }
+
+        // token中的userId和数据库中userId是否一致
+        if (sysUserTokenEntity.getUserId() != Long.parseLong(jwtUtils.getUserId(token))) {
+            WebUtils.write(response, JSONUtils.beanToJson(RestApiConstant.TokenErrorEnum.TOKEN_INVALID.getResp()));
+            return false;
+        }
+
         // token过期
         if (TokenUtils.isExpired(sysUserTokenEntity.getGmtExpire())) {
             WebUtils.write(response, JSONUtils.beanToJson(RestApiConstant.TokenErrorEnum.TOKEN_EXPIRED.getResp()));
             return false;
         }
+
         // 用户校验
         SysUserEntity sysUserEntity = userService.getUserByIdForToken(sysUserTokenEntity.getUserId());
         if (sysUserEntity.getStatus() == 0) {
             WebUtils.write(response, JSONUtils.beanToJson(RestApiConstant.TokenErrorEnum.USER_DISABLE.getResp()));
             return false;
         }
+
         return true;
     }
 

+ 55 - 0
src/main/java/net/chenlin/dp/common/support/properties/JwtProperties.java

@@ -0,0 +1,55 @@
+package net.chenlin.dp.common.support.properties;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * jwt属性配置
+ * @author zhouchenglin[yczclcn@163.com]
+ */
+@Configuration
+@ConfigurationProperties(prefix = JwtProperties.PREFIX)
+public class JwtProperties {
+
+    static final String PREFIX = "jwt";
+
+    /**
+     * jwt密钥
+     */
+    private String secret = "dp";
+
+    /**
+     * 过期时间:604800 s(单位:秒)
+     */
+    private Long expiration = 604800L;
+
+    /**
+     * md5加密混淆key
+     */
+    private String md5Key = "randomKey";
+
+    public String getSecret() {
+        return secret;
+    }
+
+    public void setSecret(String secret) {
+        this.secret = secret;
+    }
+
+    public Long getExpiration() {
+        return expiration;
+    }
+
+    public void setExpiration(Long expiration) {
+        this.expiration = expiration;
+    }
+
+    public String getMd5Key() {
+        return md5Key;
+    }
+
+    public void setMd5Key(String md5Key) {
+        this.md5Key = md5Key;
+    }
+
+}

+ 152 - 0
src/main/java/net/chenlin/dp/common/utils/JwtUtils.java

@@ -0,0 +1,152 @@
+package net.chenlin.dp.common.utils;
+
+import io.jsonwebtoken.*;
+import net.chenlin.dp.common.support.properties.JwtProperties;
+import org.apache.commons.codec.binary.Base64;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * jwt工具类
+ * @author zhouchenglin[yczclcn@163.com]
+ */
+@Component
+public class JwtUtils {
+
+    @Autowired
+    private JwtProperties jwtProperties;
+
+    /**
+     * 获取用户名
+     * @param token
+     * @return
+     */
+    public String getUsername(String token) {
+        return getClaims(token).getSubject();
+    }
+
+    /**
+     * 创建时间
+     * @param token
+     * @return
+     */
+    public Date getCreateDate(String token) {
+        return getClaims(token).getIssuedAt();
+    }
+
+    /**
+     * 过期时间
+     * @param token
+     * @return
+     */
+    public Date getExpireDate(String token) {
+        return getClaims(token).getExpiration();
+    }
+
+    /**
+     * token接收者
+     * @param token
+     * @return
+     */
+    public String getUserId(String token) {
+        return getClaims(token).getAudience();
+    }
+
+    /**
+     * 获取md5混淆的key
+     * @param token
+     * @return
+     */
+    public String getMd5Key(String token) {
+        return getPrivateClaims(token, jwtProperties.getMd5Key());
+    }
+
+    /**
+     * 获取私有的claims
+     * @param token
+     * @param key
+     * @return
+     */
+    private String getPrivateClaims(String token, String key) {
+        return getClaims(token).get(key).toString();
+    }
+
+    /**
+     * 获取payload
+     * @param token
+     * @return
+     */
+    private Claims getClaims(String token) {
+        return Jwts.parser()
+                .setSigningKey(getSecretByteArr())
+                .parseClaimsJws(token)
+                .getBody();
+    }
+
+    /**
+     * 解析token是否正确
+     * @param token
+     * @throws JwtException
+     */
+    public void parseToken(String token) throws JwtException {
+        Jwts.parser().setSigningKey(getSecretByteArr()).parseClaimsJws(token).getBody();
+    }
+
+    /**
+     * 验证token是否过期
+     * @param token
+     * @return
+     */
+    public boolean isExpred(String token) {
+        try {
+            final Date expireDate = getExpireDate(token);
+            return expireDate.before(new Date());
+        } catch (ExpiredJwtException e) {
+            return true;
+        }
+    }
+
+    /**
+     * 生成token,用户名
+     * @param username
+     * @return
+     */
+    public String generateToken(String username, String userId, String randomKey) {
+        Map<String, Object> claims = new HashMap<>(1);
+        claims.put(jwtProperties.getMd5Key(), randomKey);
+        return generateToken(claims, username, userId);
+    }
+
+    /**
+     * 生成token
+     * @param claims
+     * @param username 用户名
+     * @param userId 用户id
+     * @return
+     */
+    public String generateToken(Map<String, Object> claims, String username, String userId) {
+        final Date currDate = new Date();
+        final Date expireDate = new Date(currDate.getTime() + jwtProperties.getExpiration() * 1000);
+        return Jwts.builder()
+                .setClaims(claims)
+                .setAudience(userId)
+                .setSubject(username)
+                .setIssuedAt(currDate)
+                .setExpiration(expireDate)
+                .signWith(SignatureAlgorithm.HS256, getSecretByteArr())
+                .compact();
+    }
+
+    /**
+     * 密钥base64数组
+     * @return
+     */
+    private byte[] getSecretByteArr() {
+        return Base64.decodeBase64(jwtProperties.getSecret());
+    }
+
+}

+ 31 - 6
src/main/java/net/chenlin/dp/modules/api/controller/RestAuthController.java

@@ -6,6 +6,8 @@ import io.swagger.annotations.ApiOperation;
 import io.swagger.annotations.ApiParam;
 import net.chenlin.dp.common.constant.RestApiConstant;
 import net.chenlin.dp.common.entity.R;
+import net.chenlin.dp.common.utils.CommonUtils;
+import net.chenlin.dp.common.utils.JwtUtils;
 import net.chenlin.dp.common.utils.MD5Utils;
 import net.chenlin.dp.common.utils.TokenUtils;
 import net.chenlin.dp.modules.sys.controller.AbstractController;
@@ -30,13 +32,16 @@ public class RestAuthController extends AbstractController {
     @Autowired
     private SysUserService sysUserService;
 
+    @Autowired
+    private JwtUtils jwtUtils;
+
     /**
      * 登录授权校验
      * @return
      */
     @ApiOperation(value = "登录")
     @ApiImplicitParam(name = "token", value = "授权码")
-    @RequestMapping(value = RestApiConstant.AUTH_REQUEST, method = RequestMethod.POST)
+    @RequestMapping(value = RestApiConstant.AUTH_REQUEST, method = {RequestMethod.GET, RequestMethod.POST})
     public R auth(@ApiParam(name = "username", value = "用户名") @RequestParam String username,
                   @ApiParam(name = "password", value = "密码") @RequestParam String password) {
         // 用户名为空
@@ -62,8 +67,10 @@ public class RestAuthController extends AbstractController {
             return RestApiConstant.TokenErrorEnum.USER_DISABLE.getResp();
         }
         // 保存或者更新token
-        String token = TokenUtils.generateValue();
-        int count = sysUserService.saveOrUpdateToken(sysUserEntity.getUserId(), token);
+        String randomKey = TokenUtils.generateValue();
+        String token = jwtUtils.generateToken(sysUserEntity.getUsername(),
+                String.valueOf(sysUserEntity.getUserId()), randomKey);
+        int count = sysUserService.saveOrUpdateToken(sysUserEntity.getUserId(), randomKey);
         if (count > 0) {
             R success = RestApiConstant.TokenErrorEnum.TOKEN_ENABLE.getResp();
             success.put(RestApiConstant.AUTH_TOKEN, token);
@@ -77,30 +84,48 @@ public class RestAuthController extends AbstractController {
      * @return
      */
     @ApiOperation(value = "校验token是否可用")
-    @RequestMapping(value = RestApiConstant.AUTH_CHECK, method = RequestMethod.POST)
+    @RequestMapping(value = RestApiConstant.AUTH_CHECK, method = {RequestMethod.GET, RequestMethod.POST})
     public R authStatus(@ApiParam(name = "token", value = "授权码") @RequestParam String token) {
+
         // token为空
         if (StringUtils.isBlank(token.trim())) {
             return RestApiConstant.TokenErrorEnum.TOKEN_NOT_FOUND.getResp();
         }
-        SysUserTokenEntity sysUserTokenEntity = sysUserService.getUserTokenByToken(token);
+
+        // jwt过期时间校验
+        if (jwtUtils.isExpred(token)) {
+            return RestApiConstant.TokenErrorEnum.TOKEN_EXPIRED.getResp();
+        }
+
+        // 根据md5混淆字符串查询用户token
+        SysUserTokenEntity sysUserTokenEntity = sysUserService.getUserTokenByToken(jwtUtils.getMd5Key(token));
+
         // 无效的token:token不存在
         if (sysUserTokenEntity == null) {
             return RestApiConstant.TokenErrorEnum.TOKEN_INVALID.getResp();
         }
+
+        // token中的userId和数据库中userId是否一致
+        if (sysUserTokenEntity.getUserId() != Long.parseLong(jwtUtils.getUserId(token))) {
+            return RestApiConstant.TokenErrorEnum.TOKEN_INVALID.getResp();
+        }
+
         // 无效token:用户不存在
         SysUserEntity sysUserEntity = sysUserService.getUserByIdForToken(sysUserTokenEntity.getUserId());
         if (sysUserEntity == null) {
             return RestApiConstant.TokenErrorEnum.TOKEN_INVALID.getResp();
         }
-        // token过期
+
+        // token过期:采用服务端时间校验
         if (TokenUtils.isExpired(sysUserTokenEntity.getGmtExpire())) {
             return RestApiConstant.TokenErrorEnum.TOKEN_EXPIRED.getResp();
         }
+
         // 用户是否禁用
         if (sysUserEntity.getStatus() == 0) {
             return RestApiConstant.TokenErrorEnum.USER_DISABLE.getResp();
         }
+
         return RestApiConstant.TokenErrorEnum.TOKEN_ENABLE.getResp();
     }
 

+ 6 - 2
src/main/java/net/chenlin/dp/modules/sys/service/impl/SysUserServiceImpl.java

@@ -1,10 +1,10 @@
 package net.chenlin.dp.modules.sys.service.impl;
 
-import net.chenlin.dp.common.constant.RestApiConstant;
 import net.chenlin.dp.common.constant.SystemConstant;
 import net.chenlin.dp.common.entity.Page;
 import net.chenlin.dp.common.entity.Query;
 import net.chenlin.dp.common.entity.R;
+import net.chenlin.dp.common.support.properties.JwtProperties;
 import net.chenlin.dp.common.utils.CommonUtils;
 import net.chenlin.dp.common.utils.MD5Utils;
 import net.chenlin.dp.modules.sys.dao.*;
@@ -39,6 +39,9 @@ public class SysUserServiceImpl implements SysUserService {
 	@Autowired
 	private SysUserTokenMapper sysUserTokenMapper;
 
+	@Autowired
+	private JwtProperties jwtProperties;
+
 	/**
 	 * 分页查询用户列表
 	 * @param params
@@ -235,7 +238,7 @@ public class SysUserServiceImpl implements SysUserService {
 	@Override
 	public int saveOrUpdateToken(Long userId, String token) {
 		Date now = new Date();
-		Date expire = new Date(now.getTime() + RestApiConstant.TOKEN_EXPIRE);
+		Date expire = new Date(now.getTime() + jwtProperties.getExpiration() * 1000);
 		SysUserTokenEntity sysUserTokenEntity = new SysUserTokenEntity();
 		sysUserTokenEntity.setUserId(userId);
 		sysUserTokenEntity.setGmtModified(now);
@@ -273,6 +276,7 @@ public class SysUserServiceImpl implements SysUserService {
 	 * @param userId
 	 * @return
 	 */
+	@Override
 	public SysUserEntity getUserByIdForToken(Long userId) {
 		return sysUserMapper.getObjectById(userId);
 	}

+ 5 - 0
src/main/resources/application.yml

@@ -15,6 +15,11 @@ global:
   redis-session-dao: false #是否使用使用redis会话管理器,true为开启,false为关闭
   kaptcha-enable: true #是否开启验证码,true为开启,false为关闭
 
+# jwt配置
+jwt:
+  secret: dp  #jwt密钥
+  expiration: 604800  #过期时间,秒
+
 spring:
   # 环境 sit:集成测试环境|pre:预生产环境|prd:生成环境
   profiles:

+ 56 - 0
src/test/java/net/chenlin/dp/common/utils/JwtUtilsTest.java

@@ -0,0 +1,56 @@
+package net.chenlin.dp.common.utils;
+
+import net.chenlin.dp.common.support.properties.JwtProperties;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.Assert;
+import org.testng.annotations.BeforeTest;
+import org.testng.annotations.Test;
+
+/**
+ * JwtUtilsTest
+ * @author zhouchenglin[yczclcn@163.com]
+ */
+public class JwtUtilsTest {
+
+    private static final Logger log = LoggerFactory.getLogger(JwtUtilsTest.class);
+
+    @InjectMocks
+    JwtUtils jwtUtils = new JwtUtils();
+
+    @Mock
+    private JwtProperties jwtProperties;
+
+    @BeforeTest
+    public void init() {
+        MockitoAnnotations.initMocks(this);
+        Mockito.when(jwtProperties.getSecret()).thenReturn("dp");
+        Mockito.when(jwtProperties.getExpiration()).thenReturn(604800L);
+        Mockito.when(jwtProperties.getMd5Key()).thenReturn("randomKey");
+    }
+
+    @Test
+    public void test() {
+        String username = "admin", userId = "111", randomKey = TokenUtils.generateValue();
+        String token = jwtUtils.generateToken(username, userId, randomKey);
+        log.info("用户名:{},生成token:{}", username, token);
+
+        // 用户名
+        String jwtSubject = jwtUtils.getUsername(token);
+        Assert.assertEquals(jwtSubject, username);
+
+        // 用户id
+        String jwtUserId = jwtUtils.getUserId(token);
+        Assert.assertEquals(jwtUserId, userId);
+
+        // 混淆密钥
+        String jwtRadomKey = jwtUtils.getMd5Key(token);
+        Assert.assertEquals(jwtRadomKey, randomKey);
+
+    }
+
+}