代码略多,粘贴一些关键的代码,完整demo当然必须放在GitHub上面啦,当然带SQL文件的,在项目里面
GitHub地址: https://github.com/zhang-xiaoxiang/shiro-jwt
说明:由于初衷是解决自己项目的bug的,就找的网上的一面博客瞎搞了一个demo.然后报的错网上难以找到解决办法,后来自己解决了,就记录一下,所以不算教程,我看评论大伙说的修改密码后之前的token仍然有效,可以使用redis处理,或者其他业务逻辑代码处理一下,这个仅仅是一个demo,当然很多培训机构的源码也有一些demo,大家可以参考,这里主要是给需要的小伙伴一个思路或者遇到那个bug的解决方案
关于这个例子介绍:
1使用的是spring boot2作为环境基础
2数据库框架是mybatis plus(对mybatis熟悉就行,你顺便看一下mybatis plus逆向生成代码的魅力)
3包含全局异常的处理(报错getWriter() has already been called for this response),这个BUG是网上的访问量比较高的CSDN的博客案例留下的bug,为了解决这个bug才有我的这篇博客,所以记录一下
4shiro可以实现除了验证和授权之外的一些功能,这里主要展示验证,验证相当于登录,授权相当于登录后查看是否有权限,就想即使登录了支付宝,但是付款的时候还是需要验证支付宝密码一样,当然前后端未分离可以使用thymeleaf网页引擎,貌似黑马视频有,这里就只做个登录例子
5jwt这种方式是不太时尚的,正确的方式spring security oauth2实现认证授权,这里仅供参考,欢迎小伙伴提出宝贵意见,我都会尽快回复
6使用的是mysql8,pom和properties文件你改成现在用得多一点的5.7版本的写法就行了
7这里根据博友评论确实忘了用户修改密码token的处理,我相信这个大家应该会处理的(更新一下token就行了)
主要步骤:
首先是pom引入(主要的依赖,由于代码较大,而且我提供了Git地址,需要的直接从GitHub拉下来就行了,后面不做过多阐述)
com.auth0
java-jwt
3.4.0
org.apache.shiro
shiro-spring
1.4.0
实现shiro的AuthenticationToken接口的类JwtToken
//package com.example.shirojwt.jwt;
import org.apache.shiro.authc.AuthenticationToken;
/**
* JwtToken:实现shiro的AuthenticationToken接口的类JwtToken
*
* @author zhangxiaoxiang
* @date: 2019/07/12
*/
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;
}
}
再写一个token的验证工具类
//package com.example.shirojwt.util;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.interfaces.DecodedJWT;
import lombok.extern.slf4j.Slf4j;
import java.util.Date;
/**
* JwtUtil:用来进行签名和效验Token
*
* @author zhangxiaoxiang
* @date: 2019/07/12
*/
@Slf4j
public class JwtUtil {
/**
* JWT验证过期时间 EXPIRE_TIME 分钟
*/
private static final long EXPIRE_TIME = 30 * 60 * 1000;
/**
* 校验token是否正确
*
* @param token 密钥
* @param secret 用户的密码
* @return 是否正确
*/
public static boolean verify(String token, String username, String secret) {
try {
//根据密码生成JWT效验器
Algorithm algorithm = Algorithm.HMAC256(secret);
JWTVerifier verifier = JWT.require(algorithm)
.withClaim("username", username)
.build();
//效验TOKEN
DecodedJWT jwt = verifier.verify(token);
log.info("登录验证成功!");
return true;
} catch (Exception exception) {
log.error("JwtUtil登录验证失败!");
return false;
}
}
/**
* 获得token中的信息无需secret解密也能获得
*
* @return token中包含的用户名
*/
public static String getUsername(String token) {
try {
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim("username").asString();
} catch (JWTDecodeException e) {
return null;
}
}
/**
* 生成token签名EXPIRE_TIME 分钟后过期
*
* @param username 用户名(电话号码)
* @param secret 用户的密码
* @return 加密的token
*/
public static String sign(String username, String secret) {
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
Algorithm algorithm = Algorithm.HMAC256(secret);
// 附带username信息
return JWT.create()
.withClaim("username", username)
.withExpiresAt(date)
.sign(algorithm);
}
public static void main(String[] args) {
/**
* 测试生成一个token
*/
String sign = sign("18888888888", "123456");
log.warn("测试生成一个token\n"+sign);
}
}
写一个jwt过滤器来作为shiro的过滤器。
//package com.example.shirojwt.filter;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.example.shirojwt.jwt.JwtToken;
import com.example.shirojwt.result.ResponseData;
import com.example.shirojwt.result.ResponseDataUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMethod;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* JwtFilter:jwt过滤器来作为shiro的过滤器
*
* @author zhangxiaoxiang
* @date: 2019/07/12
*/
@Slf4j
@Component//这个注入与否影响不大
public class JwtFilter extends BasicHttpAuthenticationFilter implements Filter {
/**
* 执行登录
* @param request
* @param response
* @return
* @throws Exception
*/
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String token = httpServletRequest.getHeader("Token");
JwtToken jwtToken = new JwtToken(token);
// 提交给realm进行登入,如果错误他会抛出异常并被捕获
try {
getSubject(request, response).login(jwtToken);
// 如果没有抛出异常则代表登入成功,返回true
return true;
} catch (AuthenticationException e) {
ResponseData responseData = ResponseDataUtil.authorizationFailed( "没有访问权限,原因是:" + e.getMessage());
//SerializerFeature.WriteMapNullValue为了null属性也输出json的键值对
Object o = JSONObject.toJSONString(responseData, SerializerFeature.WriteMapNullValue);
response.setCharacterEncoding("utf-8");
response.getWriter().print(o);
return false;
}
}
/**
* 执行登录认证
*
* @param request
* @param response
* @param mappedValue
* @return
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
try {
return executeLogin(request, response);
// return true;有一篇博客这里直接返回true是不正确的,在这里我特别指出一下
} catch (Exception e) {
log.error("JwtFilter过滤验证失败!");
return false;
}
}
/**
* 对跨域提供支持
* @param request
* @param response
* @return
* @throws Exception
*/
@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);
}
}
自定义Realm
//package com.example.shirojwt.shiro;
import com.example.shirojwt.entity.User;
import com.example.shirojwt.jwt.JwtToken;
import com.example.shirojwt.service.UserService;
import com.example.shirojwt.util.JwtUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* MyRealm:自定义一个授权
*
* @author zhangxiaoxiang
* @date: 2019/07/12
*/
@Component
@Slf4j
public class MyRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
/**
* 必须重写此方法,不然Shiro会报错
* @param token
* @return
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}
/**
* 只有当需要检测用户权限的时候才会调用此方法,例如checkRole,checkPermission之类的
* @param principals
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String username = JwtUtil.getUsername(principals.toString());
User user = userService.findByUserName(username);
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
return simpleAuthorizationInfo;
}
/**
* 默认使用此方法进行用户名正确与否验证,错误抛出异常即可。
* @param auth
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
String token = (String) auth.getCredentials();
// 解密获得username,用于和数据库进行对比
String username = null;
try {
//这里工具类没有处理空指针等异常这里处理一下(这里处理科学一些)
username = JwtUtil.getUsername(token);
} catch (Exception e) {
throw new AuthenticationException("heard的token拼写错误或者值为空");
}
if (username == null) {
log.error("token无效(空''或者null都不行!)");
throw new AuthenticationException("token无效");
}
User userBean = userService.findByUserName(username);
if (userBean == null) {
log.error("用户不存在!)");
throw new AuthenticationException("用户不存在!");
}
if (!JwtUtil.verify(token, username, userBean.getUserPassword())) {
log.error("用户名或密码错误(token无效或者与登录者不匹配)!)");
throw new AuthenticationException("用户名或密码错误(token无效或者与登录者不匹配)!");
}
return new SimpleAuthenticationInfo(token, token, "my_realm");
}
}
ShiroConfig核心配置
//package com.example.shirojwt.config;
import com.example.shirojwt.filter.JwtFilter;
import com.example.shirojwt.shiro.MyRealm;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* ShiroConfig:shiro 配置类,配置哪些拦截,哪些不拦截,哪些授权等等各种配置都在这里
*
* 很多都是老套路,按照这个套路配置就行了
*
* @author zhangxiaoxiang
* @date: 2019/07/12
*/
@Configuration
public class ShiroConfig {
/**
* 注入安全过滤器
* @param securityManager
* @return
*/
@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
//拦截器
Map filterChainDefinitionMap = new LinkedHashMap();
// 配置不会被拦截的链接 顺序判断
filterChainDefinitionMap.put("/login/**", "anon");
filterChainDefinitionMap.put("/**.js", "anon");
filterChainDefinitionMap.put("/druid/**", "anon");
filterChainDefinitionMap.put("/swagger**/**", "anon");
filterChainDefinitionMap.put("/**/swagger**/**", "anon");
filterChainDefinitionMap.put("/webjars/**", "anon");
filterChainDefinitionMap.put("/v2/**", "anon");
// 添加自己的过滤器并且取名为jwt
Map filterMap = new HashMap(1);
filterMap.put("jwt", new JwtFilter());
shiroFilterFactoryBean.setFilters(filterMap);
//