一般服务的安全包括认证(Authentication)与授权(Authorization)两部分,认证即证明一个用户是合法的用户,比如通过用户名密码的形式,授权则是控制某个用户可以访问哪些资源。比较成熟的框架有Shiro、Spring Security,如果要实现第三方授权模式,则可采用OAuth2。但如果是一些简单的应用,比如一个只需要鉴别用户是否登录的APP,则可以简单地通过注解+拦截器的方式来实现。本文介绍了具体实现过程,虽基于Spring Boot实现,但稍作修改(主要是拦截器配置)就可以引入其它Spring MVC的项目。
1. 依赖配置
在pom.xml中添加JWT与redis依赖
<?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">
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.5.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>springboot-auth</artifactId>
<properties>
<java.version>1.8</java.version>
<jwt.version>0.9.1</jwt.version>
<lombok.version>1.16.18</lombok.version>
<apache-commons-lang3.version>3.7</apache-commons-lang3.version>
<commons-collections.version>3.2.2</commons-collections.version>
<commons-io.version>2.6</commons-io.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>${jwt.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>${apache-commons-lang3.version}</version>
</dependency>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>${commons-collections.version}</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>${commons-io.version}</version>
</dependency>
</dependencies>
</project>
在application.yml配置文件中添加redis相关配置属性
spring:
redis:
host: localhost
port: 6379
database: 0
password:
timeout: 3000
jedis:
pool:
min-idle: 2
max-idle: 8
max-active: 8
max-wait: 1000
jwt:
secret: mySecret
expirt: 3600
authorization: access-token
2. 自定义注解
比如需要登录的接口比较多,就可以定义如 @NoAuth 的注解来标记不需要登录验证,反之则需要鉴权。
package com.kongliand.auth;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/***
* 不会检查用户是否登录及鉴权
* @author kevin
* @Date 2020/7/14 20:40
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface NoAuth {
}
3. 定义token管理器
在登录接口通过时,调用 createToken
创建token,并保存到redis中,设置过期时间,调用 checkToken
来验证,并更新token的过期时间, 退出登录时,删除token。
package com.kongliand.auth;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.concurrent.TimeUnit;
/***
* redis token管理工具
* @author kevin
* @Date 2020/7/14 20:40
*/
@Component
public class RedisTokenManager {
@Autowired
private StringRedisTemplate redisTemplate;
@Value("${jwt.secret}")
private String jwtSecret;
@Value("${jwt.expirt}")
private Long expirtTime;
@Value("${jwt.authorization}")
private String access_Token;
/**
* 生成TOKEN
*/
public String createToken(String userId) {
String token = Jwts.builder().setId(userId).setIssuedAt(new Date()).signWith(SignatureAlgorithm.HS256, jwtSecret).compact();
//存储到redis并设置过期时间
redisTemplate.boundValueOps(access_Token + ":" + userId).set(token, expirtTime, TimeUnit.SECONDS);
return token;
}
public boolean checkToken(TokenModel model) {
if (model == null) {
return false;
}
String token = redisTemplate.boundValueOps(access_Token + ":" + model.getUserId()).get();
if (token == null || !token.equals(model.getToken())) {
return false;
}
//如果验证成功,说明此用户进行了一次有效操作,延长token的过期时间
redisTemplate.boundValueOps(model.getUserId()).expire(expirtTime,TimeUnit.SECONDS);
return true;
}
public void deleteToken(String userId) {
redisTemplate.delete(userId);
}
}
4. 定义拦截器
如果验证通过,则从JWT token中解析出userId,通过AuthUtil工具方法保存到ThreadLocal中,供下游访问。在请求处理结束调用 afterCompletion
方法中,要清除掉ThreadLocal中的值,否则由于线程池的复用,导致被其他用户获取。
package com.kongliand.auth;
import com.kongliand.util.ApiResponse;
import com.kongliand.util.WebUtil;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
/***
*
* @author kevin
* @Date 2020/7/14 20:40
*/
@Component
@Slf4j
public class AuthInterceptor extends HandlerInterceptorAdapter {
@Autowired
private RedisTokenManager tokenManager;
@Value("${jwt.secret}")
private String jwtSecret;
@Value("${jwt.authorization}")
private String access_Token;
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
String requestPath = request.getRequestURI().substring(request.getContextPath().length());
// 如果不是映射到方法直接通过
if (!(handler instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
// 如果方法注明了 NoAuth,则不需要登录token验证
if (method.getAnnotation(NoAuth.class) != null) {
return true;
}
// 从header中得到token
String authorization = request.getHeader(access_Token);
// 验证token
if(StringUtils.isBlank(authorization)){
WebUtil.outputJsonString(ApiResponse.failed("未提供有效Token!"), response);
return false;
}
try {
Claims claims = Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(authorization).getBody();
String userId = claims.getId();
TokenModel model = new TokenModel(userId, authorization);
if (tokenManager.checkToken(model)) {
AuthUtil.setUserId(model.getUserId());
return true;
} else {
WebUtil.outputJsonString(ApiResponse.failed("未提供有效Token!"), response);
return false;
}
} catch (Exception e) {
WebUtil.outputJsonString(ApiResponse.failed("校验Token发生异常!"), response);
return false;
}
}
@Override
public void afterCompletion(
HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
//结束后清除,否则由于连接池复用,导致ThreadLocal的值被其他用户获取
AuthUtil.clear();
}
}
注册拦截器
package com.kongliand.config;
import com.kongliand.auth.AuthInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/***
*
* @author kevin
* @Date 2020/7/14 20:40
*/
@Configuration
public class WebConfiguration implements WebMvcConfigurer {
private AuthInterceptor authInterceptor;
@Autowired
public void setAuthInterceptor(AuthInterceptor authInterceptor){
this.authInterceptor = authInterceptor;
}
/**
* 注册鉴权拦截器
* @param
* @return
*/
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authInterceptor).addPathPatterns("/**").excludePathPatterns("/error");
}
}
5. 定义测试controller
package com.kongliand.controller; import com.kongliand.auth.AuthUtil; import com.kongliand.auth.NoAuth; import com.kongliand.auth.RedisTokenManager; import com.kongliand.util.ApiResponse; import org.apache.commons.collections.MapUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.Map; /*** * 测试controller * @author kevin * @Date 2020/7/14 20:40 */ @RestController @RequestMapping("/test") public class TestContoller { @Autowired private RedisTokenManager tokenManager; @NoAuth @RequestMapping("/login") public ApiResponse login(@RequestBody Map<String, Object> params) { String username = MapUtils.getString(params, "username"); String password = MapUtils.getString(params, "password"); if ("kevin".equals(username) && "123456".equals(password)) { return ApiResponse.success(tokenManager.createToken(username)); } else { return ApiResponse.failed("用户名或密码错误"); } } @NoAuth @RequestMapping("/no-auth") public ApiResponse skipAuth() { return ApiResponse.success("不需要认证的接口调用"); } @RequestMapping("/auth") public ApiResponse needAuth() { return ApiResponse.success("username: " + AuthUtil.getUserId()); } }
6. 验证
登录接口
测试接口