这篇博客是在 Spring boot 入门教程-全局异常处理及日志输出 的基础上完成的。
我们在做项目时比如商城项目,有的页面的打开是需要登陆的而有的页面则不需要,所以这里就需要一种验证是否登录,或者登录是否过期,这里说一种token令牌+拦截器的方式。
生成token 使用JWT。
1.引入
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
2.生成token令牌的工具类
public class TokenUtils {
/**
* 签名秘钥
*/
public static final String SECRET = "admin";
/**
* 生成token
*
* @param id 一般传入userName
* @return
*/
public static String createJwtToken(String id) {
String issuer = "www.xxxx.com";
String subject = "xxxx@126.com";
long ttlMillis = 3600000;
return createJwtToken(id, issuer, subject, ttlMillis);
}
/**
* 生成Token
*
* @param id 编号
* @param issuer 该JWT的签发者,是否使用是可选的
* @param subject 该JWT所面向的用户,是否使用是可选的;
* @param ttlMillis 签发时间 (有效时间,过期会报错)
* @return token String
*/
public static String createJwtToken(String id, String issuer, String subject, long ttlMillis) {
// 签名算法 ,将对token进行签名
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
// 生成签发时间
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
// 通过秘钥签名JWT
byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(SECRET);
Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());
// Let's set the JWT Claims
JwtBuilder builder = Jwts.builder().setId(id)
.setIssuedAt(now)
.setSubject(subject)
.setIssuer(issuer)
.signWith(signatureAlgorithm, signingKey);
// if it has been specified, let's add the expiration
if (ttlMillis >= 0) {
long expMillis = nowMillis + ttlMillis;
Date exp = new Date(expMillis);
builder.setExpiration(exp);
}
// Builds the JWT and serializes it to a compact, URL-safe string
return builder.compact();
}
// Sample method to validate and read the JWT
public static Claims parseJWT(String jwt) {
// This line will throw an exception if it is not a signed JWS (as expected)
Claims claims = Jwts.parser()
.setSigningKey(DatatypeConverter.parseBase64Binary(SECRET))
.parseClaimsJws(jwt).getBody();
return claims;
}
public static void main(String[] args) {
System.out.println(TokenUtils.createJwtToken("11111"));
}
}
3.注入当前登录用户注解
/**
* @BelongsProject: JDTaste
* @BelongsPackage: com.jdtaste.common.util
* @Author:
* @CreateTime: 2018-07-04 15:39
* @Description: 在Controller的方法参数中使用此注解,该方法在映射时会注入当前登录的User对象
*/
@Target(ElementType.PARAMETER) // 可用在方法的参数上
@Retention(RetentionPolicy.RUNTIME) // 运行时有效
public @interface CurrentUser {
}
4.需要登录的标记注解
/**
* @BelongsProject: JDTaste
* @BelongsPackage: com.jdtaste.common.util
* @Author:
* @CreateTime: 2018-07-04 15:38
* @Description: 在需要登录验证的Controller的方法上使用此注解
*/
@Target({ElementType.METHOD})// 可用在方法名上
@Retention(RetentionPolicy.RUNTIME)// 运行时有效
public @interface LoginRequired {
}
5.编辑拦截器
/**
* @BelongsProject:
* @BelongsPackage: com.jdtaste.jdtastesso.web.intercepter
* @Author:
* @CreateTime: 2018-07-04 09:50
* @Description: 拦截器
*/
public class AuthenticationInterceptor implements HandlerInterceptor {
public final static String ACCESS_TOKEN = "accessToken";
@Resource
IUserBaseService userBaseService;
// 在业务处理器处理请求之前被调用
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 如果不是映射到方法直接通过
if (!(handler instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
// 判断接口是否需要登录
LoginRequired methodAnnotation = method.getAnnotation(LoginRequired.class);
// 有 @LoginRequired 注解,需要认证
if (methodAnnotation != null) {
// 判断是否存在令牌信息,如果存在,则允许登录
String accessToken = request.getHeader("Authorization");
if (null == accessToken) {
throw new CommonException(401, "无token,请重新登录");
} else {
// 从Redis 中查看 token 是否过期
Claims claims;
try{
claims = TokenUtils.parseJWT(accessToken);
}catch (ExpiredJwtException e){
response.setStatus(401);
throw new CommonException(401, "token失效,请重新登录");
}catch (SignatureException se){
response.setStatus(401);
throw new CommonException(401, "token令牌错误");
}
String userName = claims.getId();
UserBase user = userBaseService.findUserByAccount(userName);
if (user == null) {
response.setStatus(401);
throw new CommonException(401, "用户不存在,请重新登录");
}
// 当前登录用户@CurrentUser
request.setAttribute(CurrentUserConstants.CURRENT_USER, user);
return true;
}
} else {//不需要登录可请求
return true;
}
}
// 请求处理之后进行调用,但是在视图被渲染之前(Controller方法调用之后)
@Override
public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
}
// 在整个请求结束之后被调用,也就是在DispatcherServlet 渲染了对应的视图之后执行(主要是用于进行资源清理工作)
@Override
public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
}
}
/**
* @BelongsProject:
* @BelongsPackage: com.jdtaste.jdtastesso.web.intercepter.auth
* @Author:
* @CreateTime: 2018-07-04 15:45
* @Description: 当前用户
*/
public class CurrentUserConstants {
/**
* 当前用户参数名
*/
public final static String CURRENT_USER = "CurrentUser";
}
6.自定义参数解析器
/**
*
* @BelongsPackage: com.jdtaste.jdtastesso.web.intercepter.auth
* @Author:
* @CreateTime: 2018-07-04 15:42
* @Description: 自定义参数解析器
* 增加方法注入,将含有 @CurrentUser 注解的方法参数注入当前登录用户
*/
public class CurrentUserMethodArgumentResolver implements HandlerMethodArgumentResolver {
/*
* supportsParameter:用于判定是否需要处理该参数分解,返回true为需要,并会去调用下面的方法resolveArgument。
*resolveArgument:真正用于处理参数分解的方法,返回的Object就是controller方法上的形参对象。
*
* */
@Override
public boolean supportsParameter(MethodParameter parameter) {
System.out.println("----------supportsParameter-----------" + parameter.getParameterType());
return parameter.getParameterType().isAssignableFrom(UserBase.class)//判断是否能转成UserBase 类型
&& parameter.hasParameterAnnotation(CurrentUser.class);//是否有CurrentUser注解
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
System.out.println("--------------resolveArgument-------------" + parameter);
UserBase user = (UserBase) webRequest.getAttribute(CurrentUserConstants.CURRENT_USER, RequestAttributes.SCOPE_REQUEST);
if (user != null) {
return user;
}
throw new MissingServletRequestPartException(CurrentUserConstants.CURRENT_USER);
}
}
7.将拦截器和参数解析器加入容器
/**
* @BelongsProject:
* @BelongsPackage: com.jdtaste.jdtastesso.conf
* @Author:
* @CreateTime: 2018-07-04 10:03
* @Description: 配置URLInterceptor拦截器,以及拦截路径
*/
@EnableWebMvc
@Configuration
public class MyWebAppConfigurer extends WebMvcConfigurerAdapter {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// addPathPatterns 用于添加拦截规则
// excludePathPatterns 用户排除拦截
registry.addInterceptor(authenticationInterceptor())
.addPathPatterns("/*/*");
super.addInterceptors(registry);
}
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(currentUserMethodArgumentResolver());
super.addArgumentResolvers(argumentResolvers);
}
@Bean
public CurrentUserMethodArgumentResolver currentUserMethodArgumentResolver() {
return new CurrentUserMethodArgumentResolver();
}
/**
* 解决 拦截器中注入bean 失败情况出现
* addArgumentResolvers方法中 添加
* argumentResolvers.add(currentUserMethodArgumentResolver());
*/
@Bean
public AuthenticationInterceptor authenticationInterceptor() {
return new AuthenticationInterceptor();
}
}
argumentResolvers.add(currentUserMethodArgumentResolver());
super.addArgumentResolvers(argumentResolvers);
}
@Bean
public CurrentUserMethodArgumentResolver currentUserMethodArgumentResolver() {
return new CurrentUserMethodArgumentResolver();
}
/**
* 解决 拦截器中注入bean 失败情况出现
* addArgumentResolvers方法中 添加
* argumentResolvers.add(currentUserMethodArgumentResolver());
*/
@Bean
public AuthenticationInterceptor authenticationInterceptor() {
return new AuthenticationInterceptor();
}
}
8.Controller
@LoginRequired
@RequestMapping(value = "/token")
public String token(@CurrentUser UserBase userBase,String account,String token) {
log.info(account+"----"+token);
log.info("----"+userBase.getAccount());
log.info("params==" + userBase.toString());
if (userBaseService.findUserByAccount(userBase.getAccount()) == null) {
return "账号不存在";
} else {
UserBase result = null;
result = userBaseService.login(userBase);
//生成token
//String accessToken=TokenUtils.createJwtToken(userBase.getAccount());
if (result == null) {
return "密码错误";
} else {
return "SUCCESS";
}
}
}
@LoginRequired
@RequestMapping(value = "/token")
public String token(@CurrentUser UserBase userBase,String account,String token) {
log.info(account+"----"+token);
log.info("----"+userBase.getAccount());
log.info("params==" + userBase.toString());
if (userBaseService.findUserByAccount(userBase.getAccount()) == null) {
return "账号不存在";
} else {
UserBase result = null;
result = userBaseService.login(userBase);
//生成token
//String accessToken=TokenUtils.createJwtToken(userBase.getAccount());
if (result == null) {
return "密码错误";
} else {
return "SUCCESS";
}
}
}
9.前端发送请求 (axios)
_this.$http.post('/api/user/login',
this.login,
{
headers: {
Authorization: JSON.parse(sessionStorage.getItem("loginUser")).accessToken,
}
}).then((res) => {
console.log(res);
});
10 .请求过程
拦截器-->参数解析器-->controler-->拦截器
注意:参数解析器当第一个返回true 才执行第二个方法