代码略多,粘贴一些关键的代码,完整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拉下来就行了,后面不做过多阐述)
<!--JWT java web token 权限验证--> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId>
实现shiro的AuthenticationToken接口的类JwtToken
//package com.example.shirojwt.jwt; import org.apache.shiro.authc.AuthenticationToken; * JwtToken:实现shiro的AuthenticationToken接口的类JwtToken public class JwtToken implements AuthenticationToken{ public JwtToken(String token) { public Object getPrincipal() { public Object getCredentials() {
再写一个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; * JWT验证过期时间 EXPIRE_TIME 分钟 private static final long EXPIRE_TIME = 30 * 60 * 1000; public static boolean verify(String token, String username, String secret) { Algorithm algorithm = Algorithm.HMAC256(secret); JWTVerifier verifier = JWT.require(algorithm) .withClaim("username", username) DecodedJWT jwt = verifier.verify(token); } catch (Exception exception) { log.error("JwtUtil登录验证失败!"); * 获得token中的信息无需secret解密也能获得 public static String getUsername(String token) { DecodedJWT jwt = JWT.decode(token); return jwt.getClaim("username").asString(); } catch (JWTDecodeException e) { * 生成token签名EXPIRE_TIME 分钟后过期 * @param username 用户名(电话号码) public static String sign(String username, String secret) { Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME); Algorithm algorithm = Algorithm.HMAC256(secret); .withClaim("username", username) public static void main(String[] args) { 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.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; * JwtFilter:jwt过滤器来作为shiro的过滤器 public class JwtFilter extends BasicHttpAuthenticationFilter implements Filter { protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception { HttpServletRequest httpServletRequest = (HttpServletRequest) request; String token = httpServletRequest.getHeader("Token"); JwtToken jwtToken = new JwtToken(token); // 提交给realm进行登入,如果错误他会抛出异常并被捕获 getSubject(request, response).login(jwtToken); // 如果没有抛出异常则代表登入成功,返回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); protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { return executeLogin(request, response); // return true;有一篇博客这里直接返回true是不正确的,在这里我特别指出一下 log.error("JwtFilter过滤验证失败!"); 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 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; public class MyRealm extends AuthorizingRealm { private UserService userService; public boolean supports(AuthenticationToken token) { return token instanceof JwtToken; * 只有当需要检测用户权限的时候才会调用此方法,例如checkRole,checkPermission之类的 protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { String username = JwtUtil.getUsername(principals.toString()); User user = userService.findByUserName(username); SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); return simpleAuthorizationInfo; * 默认使用此方法进行用户名正确与否验证,错误抛出异常即可。 * @throws AuthenticationException protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException { String token = (String) auth.getCredentials(); // 解密获得username,用于和数据库进行对比 //这里工具类没有处理空指针等异常这里处理一下(这里处理科学一些) username = JwtUtil.getUsername(token); throw new AuthenticationException("heard的token拼写错误或者值为空"); log.error("token无效(空''或者null都不行!)"); throw new AuthenticationException("token无效"); User userBean = userService.findByUserName(username); 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; * ShiroConfig:shiro 配置类,配置哪些拦截,哪些不拦截,哪些授权等等各种配置都在这里 public class ShiroConfig { public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>(); 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"); Map<String, Filter> filterMap = new HashMap<String, Filter>(1); filterMap.put("jwt", new JwtFilter()); shiroFilterFactoryBean.setFilters(filterMap); //<!-- 过滤链定义,从上向下顺序执行,一般将/**放在最为下边 filterChainDefinitionMap.put("/**", "jwt"); shiroFilterFactoryBean.setUnauthorizedUrl("/403"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return shiroFilterFactoryBean; public SecurityManager securityManager(MyRealm myRealm) { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(myRealm); * 关闭shiro自带的session,详情见文档 * http://shiro./session-management.html#SessionManagement-StatelessApplications%28Sessionless%29 DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO(); DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator(); defaultSessionStorageEvaluator.setSessionStorageEnabled(false); subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator); securityManager.setSubjectDAO(subjectDAO);
大体核心代码已经完成,项目结构如图(点击图片放大后就清晰了)
测试示例
比如模拟登录token(在JwtUtil生成一个)错误,或者token输入成token12,或者账号密码错误等等...
比如输入使用生成的token(相当于用户登录后返回的token):登录显然不需要token,登录后后台返回token,只是这里为了演示一个需要token的接口,所以把登录接口也设置了需要token,科学的方法是比如查询用户详情这个接口就必须要token,所以postman传入的token模拟的是需要token的接口而存在的,返回的token是给前端带上访问用户订单,地址的那些需要token认证的接口
当然在控制层也可以测试其他被限制访问的接口和放行的接口,以及各种异常的测试,这里不做过多赘述,感兴趣的看GitHub的demo
|