JWT介绍
Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的, 特别适用于分布式站点的单点登录(`SSO`)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息, 以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
优点
- 体积小、传输快
- 支持跨域授权,因为跨域无法共享cookie
- 分布式系统中,很好地解决了单点登录问题
缺点
因为JWT是无状态的,因此服务端无法控制已经生成的Token失效,是不可控的
使用场景
1. 认证,这是比较常见的使用场景,只要用户登录过一次系统,之后的请求都会包含签名出来的token,通过token也可以用来实现单点登录。
2. 交换信息,通过使用密钥对来安全的传送信息,可以知道发送者是谁、放置消息被篡改。
springboot集成JWT过程(注意: 使用了数据库, 先建表)
项目克隆
项目名称 springboot-jwt
地址: https://gitee.com/minili/springboot-demo.git
如果觉得该项目对你有帮助或者有疑问的话, 欢迎加星, 评论
添加表
一个是管理员表, 一个是存放token表
在项目下的db文件夹
1 SET FOREIGN_KEY_CHECKS=0; 2 3 DROP TABLE IF EXISTS `manager`; 4 CREATE TABLE `manager` ( 5 `managerId` int(5) unsigned NOT NULL AUTO_INCREMENT COMMENT '管理员id', 6 `managerName` varchar(50) NOT NULL, 7 `nickName` varchar(50) DEFAULT NULL, 8 `password` varchar(50) NOT NULL, 9 `managerLevelId` int(2) NOT NULL, 10 PRIMARY KEY (`managerId`) 11 ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT='管理员表'; 12 13 INSERT INTO `manager` VALUES ('1', 'admin', 'admin', '4297f44b13955235245b2497399d7a93', '1'); 14 INSERT INTO `manager` VALUES ('2', 'cscscs', 'cscscs', '4297f44b13955235245b2497399d7a93', '1'); 15 16 DROP TABLE IF EXISTS `managertoken`; 17 CREATE TABLE `managertoken` ( 18 `managerId` int(20) NOT NULL, 19 `token` varchar(50) NOT NULL, 20 `expireTime` varchar(15) DEFAULT NULL COMMENT '过期时间yyyyMMddHHmmss', 21 `updateTime` varchar(15) DEFAULT NULL COMMENT '更新时间yyyyMMddHHmmss', 22 PRIMARY KEY (`managerId`) 23 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
POM.XML
1 <?xml version="1.0" encoding="UTF-8"?> 2 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 3 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4 <modelVersion>4.0.0</modelVersion> 5 <groupId>com.mycom</groupId> 6 <artifactId>funfast</artifactId> 7 <version>0.0.1-SNAPSHOT</version> 8 <packaging>jar</packaging> 9 10 <name>funfast</name> 11 <description>project for Spring Boot JWT</description> 12 13 <parent> 14 <groupId>org.springframework.boot</groupId> 15 <artifactId>spring-boot-starter-parent</artifactId> 16 <version>2.0.1.RELEASE</version> 17 <relativePath/> 18 </parent> 19 20 <properties> 21 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> 22 <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> 23 <java.version>1.8</java.version> 24 <mysql-connector>5.1.38</mysql-connector> 25 <mybatis-plus-boot-starter.version>2.1.9</mybatis-plus-boot-starter.version> 26 <druid.version>1.1.10</druid.version> 27 <fastjson.version>1.2.39</fastjson.version> 28 <jwt.version>0.7.0</jwt.version> 29 </properties> 30 31 <dependencies> 32 <dependency> 33 <groupId>org.springframework.boot</groupId> 34 <artifactId>spring-boot-starter</artifactId> 35 <exclusions> 36 <exclusion> 37 <groupId>org.springframework.boot</groupId> 38 <artifactId>spring-boot-starter-logging</artifactId> 39 </exclusion> 40 </exclusions> 41 </dependency> 42 43 <!-- Spring Boot web依赖 --> 44 <dependency> 45 <groupId>org.springframework.boot</groupId> 46 <artifactId>spring-boot-starter-web</artifactId> 47 </dependency> 48 <!-- log4j2 依赖 --> 49 <dependency> 50 <groupId>org.springframework.boot</groupId> 51 <artifactId>spring-boot-starter-log4j2</artifactId> 52 </dependency> 53 <!-- Spring Boot Test 依赖 --> 54 <dependency> 55 <groupId>org.springframework.boot</groupId> 56 <artifactId>spring-boot-starter-test</artifactId> 57 <scope>test</scope> 58 </dependency> 59 <dependency> 60 <groupId>org.springframework.boot</groupId> 61 <artifactId>spring-boot-configuration-processor</artifactId> 62 <optional>true</optional> 63 </dependency> 64 65 <!-- Spring Boot JDBC 依赖 --> 66 <dependency> 67 <groupId>org.springframework.boot</groupId> 68 <artifactId>spring-boot-starter-jdbc</artifactId> 69 </dependency> 70 71 <!-- MySQL 连接驱动 依赖 --> 72 <dependency> 73 <groupId>mysql</groupId> 74 <artifactId>mysql-connector-java</artifactId> 75 <version>${mysql-connector}</version> 76 </dependency> 77 <!-- druid 连接池 依赖 --> 78 <dependency> 79 <groupId>com.alibaba</groupId> 80 <artifactId>druid-spring-boot-starter</artifactId> 81 <version>${druid.version}</version> 82 </dependency> 83 84 <!-- shiro 权限控制 --> 85 <dependency> 86 <groupId>org.apache.shiro</groupId> 87 <artifactId>shiro-spring</artifactId> 88 <version>1.4.0</version> 89 </dependency> 90 91 <!-- shiro ehcache (shiro缓存)--> 92 <dependency> 93 <groupId>org.apache.shiro</groupId> 94 <artifactId>shiro-ehcache</artifactId> 95 <version>1.4.0</version> 96 <exclusions> 97 <exclusion> 98 <artifactId>slf4j-api</artifactId> 99 <groupId>org.slf4j</groupId> 100 </exclusion> 101 </exclusions> 102 </dependency> 103 104 <!-- jwt --> 105 <dependency> 106 <groupId>io.jsonwebtoken</groupId> 107 <artifactId>jjwt</artifactId> 108 <version>${jwt.version}</version> 109 </dependency> 110 111 <!-- fastjson 依赖 --> 112 <dependency> 113 <groupId>com.alibaba</groupId> 114 <artifactId>fastjson</artifactId> 115 <version>${fastjson.version}</version> 116 </dependency> 117 118 </dependencies> 119 120 <build> 121 <plugins> 122 <plugin> 123 <groupId>org.springframework.boot</groupId> 124 <artifactId>spring-boot-maven-plugin</artifactId> 125 </plugin> 126 </plugins> 127 </build> 128 129 </project>
修改application.yml文件
1 # server 2 server: 3 tomcat: 4 uri-encoding: UTF-8 5 max-threads: 1000 6 min-spare-threads: 30 7 port: 8087 8 servlet: 9 context-path: / 10 11 spring: 12 # 环境 dev|prod 13 profiles: 14 active: dev 15 16 servlet: 17 multipart: 18 max-file-size: 100MB 19 max-request-size: 100MB 20 enabled: true 21 22 datasource: 23 type: com.alibaba.druid.pool.DruidDataSource 24 driver-class-name: com.mysql.jdbc.Driver 25 druid: 26 url: jdbc:mysql://127.0.0.1:3306/fun-fast?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull 27 username: root 28 password: 123123 29 30 initial-size: 10 31 max-active: 100 32 min-idle: 10 33 max-wait: 60000 34 pool-prepared-statements: true 35 max-pool-prepared-statement-per-connection-size: 20 36 time-between-eviction-runs-millis: 60000 37 min-evictable-idle-time-millis: 300000 38 validation-query: SELECT 1 39 test-while-idle: true 40 test-on-borrow: true 41 test-on-return: false 42 stat-view-servlet: 43 enabled: true 44 url-pattern: /druid/* 45 login-username: admin 46 login-password: 123123 47 filter: 48 stat: 49 log-slow-sql: true 50 slow-sql-millis: 1000 51 merge-sql: false 52 wall: 53 config: 54 multi-statement-allow: true
主体有5个文件需要添加,分别是shiroConfig、OAuth2Filer配置、OAuth2Realm、OAuth2Token、TokenGenerator
1. ShiroConfig配置
1 /** 2 * Shiro配置 3 */ 4 @Configuration 5 public class ShiroConfig { 6 7 @Bean("sessionManager") 8 public SessionManager sessionManager(){ 9 DefaultWebSessionManager sessionManager = new DefaultWebSessionManager(); 10 sessionManager.setSessionValidationSchedulerEnabled(true); 11 sessionManager.setSessionIdCookieEnabled(true); 12 return sessionManager; 13 } 14 15 @Bean("securityManager") 16 public SecurityManager securityManager(OAuth2Realm oAuth2Realm, SessionManager sessionManager) { 17 DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); 18 securityManager.setRealm(oAuth2Realm); 19 securityManager.setSessionManager(sessionManager); 20 21 return securityManager; 22 } 23 24 @Bean("shiroFilter") 25 public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) { 26 ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean(); 27 shiroFilter.setSecurityManager(securityManager); 28 29 //oauth过滤 30 Map<String, Filter> filters = new HashMap<>(); 31 filters.put("oauth2", new OAuth2Filter()); 32 shiroFilter.setFilters(filters); 33 34 Map<String, String> filterMap = new LinkedHashMap<>(); 35 filterMap.put("/druid/**", "anon"); 36 filterMap.put("/app/**", "anon"); 37 filterMap.put("/login", "anon"); 38 filterMap.put("/**", "oauth2"); 39 shiroFilter.setFilterChainDefinitionMap(filterMap); 40 41 return shiroFilter; 42 } 43 44 @Bean("lifecycleBeanPostProcessor") 45 public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() { 46 return new LifecycleBeanPostProcessor(); 47 } 48 49 @Bean 50 public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() { 51 DefaultAdvisorAutoProxyCreator proxyCreator = new DefaultAdvisorAutoProxyCreator(); 52 proxyCreator.setProxyTargetClass(true); 53 return proxyCreator; 54 } 55 56 @Bean 57 public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) { 58 AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor(); 59 advisor.setSecurityManager(securityManager); 60 return advisor; 61 } 62 63 }
2. OAuth2Filer配置
这个里面可以配置权限过滤的规则
1 /** 2 * oauth2过滤器 3 */ 4 public class OAuth2Filter extends AuthenticatingFilter { 5 6 @Override 7 protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception { 8 //获取请求token 9 String token = getRequestToken((HttpServletRequest) request); 10 11 if(StringUtil.isBlank(token)){ 12 return null; 13 } 14 15 return new OAuth2Token(token); 16 } 17 18 @Override 19 protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { 20 if(((HttpServletRequest) request).getMethod().equals(RequestMethod.OPTIONS.name())){ 21 return true; 22 } 23 24 return false; 25 } 26 27 @Override 28 protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { 29 //获取请求token,如果token不存在,直接返回401 30 HttpServletRequest httpServletRequest = (HttpServletRequest) request; 31 String token = getRequestToken((HttpServletRequest) request); 32 if(StringUtil.isBlank(token)){ 33 HttpServletResponse httpResponse = (HttpServletResponse) response; 34 httpResponse.setHeader("Access-Control-Allow-Credentials", "true"); 35 httpResponse.setHeader("Access-Control-Allow-Origin", httpServletRequest.getHeader("Origin")); 36 37 JSONObject json = new JSONObject(); 38 json.put("code", "401"); 39 json.put("msg", "invalid token"); 40 41 httpResponse.getWriter().print(json); 42 43 return false; 44 } 45 46 return executeLogin(request, response); 47 } 48 49 @Override 50 protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) { 51 HttpServletResponse httpResponse = (HttpServletResponse) response; 52 HttpServletRequest httpServletRequest = (HttpServletRequest) request; 53 httpResponse.setContentType("application/json;charset=utf-8"); 54 httpResponse.setHeader("Access-Control-Allow-Credentials", "true"); 55 httpResponse.setHeader("Access-Control-Allow-Origin", httpServletRequest.getHeader("Origin")); 56 try { 57 //处理登录失败的异常 58 Throwable throwable = e.getCause() == null ? e : e.getCause(); 59 60 JSONObject json = new JSONObject(); 61 json.put("code", "401"); 62 json.put("msg", throwable.getMessage()); 63 64 httpResponse.getWriter().print(json); 65 } catch (IOException e1) { 66 67 } 68 69 return false; 70 } 71 72 /** 73 * 获取请求的token 74 */ 75 private String getRequestToken(HttpServletRequest httpRequest){ 76 //从header中获取token 77 String token = httpRequest.getHeader("token"); 78 79 //如果header中不存在token,则从参数中获取token 80 if(StringUtil.isBlank(token)){ 81 token = httpRequest.getParameter("token"); 82 } 83 84 return token; 85 } 86 }
3. OAuth2Realm配置
这个里面可以设置角色、权限和认证信息
1 /** 2 * 认证 3 */ 4 @Component 5 public class OAuth2Realm extends AuthorizingRealm { 6 @Autowired 7 private ManagerService managerService; 8 9 @Override 10 public boolean supports(AuthenticationToken token) { 11 return token instanceof OAuth2Token; 12 } 13 14 /** 15 * 授权(验证权限时调用, 控制role 和 permissins时使用) 16 */ 17 @Override 18 protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { 19 ManagerInfo manager = (ManagerInfo)principals.getPrimaryPrincipal(); 20 Integer managerId = manager.getManagerId(); 21 22 SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); 23 24 // 模拟权限和角色 25 Set<String> permsSet = new HashSet<>(); 26 Set<String> roles = new HashSet<>(); 27 if (managerId == 1) { 28 // 超级管理员-权限 29 permsSet.add("delete"); 30 permsSet.add("update"); 31 permsSet.add("view"); 32 33 roles.add("admin"); 34 } else { 35 // 普通管理员-权限 36 permsSet.add("view"); 37 38 roles.add("test"); 39 } 40 41 info.setStringPermissions(permsSet); 42 info.setRoles(roles); 43 44 return info; 45 } 46 47 /** 48 * 认证(登录时调用) 49 */ 50 @Override 51 protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { 52 String accessToken = (String) token.getPrincipal(); 53 54 //根据accessToken,查询用户信息 55 ManagerToken managerToken = managerService.queryByToken(accessToken); 56 //token失效 57 SimpleDateFormat sm = new SimpleDateFormat("yyyyMMddHHmmss"); 58 Date expireTime; 59 boolean flag = true; 60 try { 61 expireTime = sm.parse(managerToken.getExpireTime()); 62 flag = managerToken == null || expireTime.getTime() < System.currentTimeMillis(); 63 } catch (ParseException e) { 64 e.printStackTrace(); 65 } 66 67 if(flag){ 68 throw new IncorrectCredentialsException("token失效,请重新登录"); 69 } 70 71 //查询用户信息 72 ManagerInfo managerInfo = managerService.getManagerInfo(managerToken.managerId); 73 //账号锁定 74 // if(managerInfo.getStatus() == 0){ 75 // throw new LockedAccountException("账号已被锁定,请联系管理员"); 76 // } 77 78 SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(managerInfo, accessToken, getName()); 79 80 return info; 81 } 82 }
4. OAuth2Token设置
1 /** 2 * token 3 */ 4 public class OAuth2Token implements AuthenticationToken { 5 private String token; 6 7 public OAuth2Token(String token){ 8 this.token = token; 9 } 10 11 @Override 12 public String getPrincipal() { 13 return token; 14 } 15 16 @Override 17 public Object getCredentials() { 18 return token; 19 } 20 }
5. TokenGenerator, 生成token
1 /** 2 * 生成token 3 */ 4 public class TokenGenerator { 5 6 public static String generateValue() { 7 return generateValue(UUID.randomUUID().toString()); 8 } 9 10 private static final char[] hexCode = "0123456789abcdef".toCharArray(); 11 12 public static String toHexString(byte[] data) { 13 if (data == null) { 14 return null; 15 } 16 StringBuilder r = new StringBuilder(data.length * 2); 17 for (byte b : data) { 18 r.append(hexCode[(b >> 4) & 0xF]); 19 r.append(hexCode[(b & 0xF)]); 20 } 21 return r.toString(); 22 } 23 24 public static String generateValue(String param) { 25 try { 26 MessageDigest algorithm = MessageDigest.getInstance("MD5"); 27 algorithm.reset(); 28 algorithm.update(param.getBytes()); 29 byte[] messageDigest = algorithm.digest(); 30 return toHexString(messageDigest); 31 } catch (Exception e) { 32 throw new RuntimeException("生成Token失败", e); 33 } 34 } 35 }
Controller
@RestController public class WebController { private static final Logger LOGGER = LogManager.getLogger(WebController.class); @Autowired private ManagerService managerService; @RequestMapping("/login") public JSONObject login(@RequestParam("username") String username, @RequestParam("password") String password) { JSONObject json = new JSONObject(); json.put("result", false); json.put("msg", "账号或密码不正确"); // 用户信息 ManagerInfo managerInfo = managerService.getManagerInfo(username); // 账号不存在、密码错误 if (managerInfo == null || !managerInfo.getPassword().equals(password)) { return json; } ManagerToken managerToken = managerService.saveToken(managerInfo.managerId); json.put("token", managerToken.token); json.put("result", true); json.put("msg", "登陆成功"); return json; } /** * 必须带token请求, 否则返回401 */ @GetMapping("/article") public BaseResponse article() { return new BaseResponse(true, "article: You are already logged in", null); } /** * 不必带token也能请求到内容, 因为在shiro中配置了过滤规则 */ @GetMapping("/app/article") public BaseResponse appArticle() { return new BaseResponse(true, "appArticle: You are already logged in", null); } /** * 需要是超级管理员的token才能查看, */ @GetMapping("/require_role") @RequiresRoles("admin") public BaseResponse requireRole() { return new BaseResponse(true, "You are visiting require_role", null); } /** * 需要有update权限才能访问 */ @GetMapping("/require_permission") // @RequiresPermissions(logical = Logical.AND, value = {"view", "edit"}) @RequiresPermissions(logical = Logical.AND, value = {"update"}) public BaseResponse requirePermission() { return new BaseResponse(true, "You are visiting permission require update", null); } }
Service
1 @Service 2 public class ManagerService extends AbstractService { 3 //12小时后过期 4 private final static int EXPIRE = 3600 * 12 * 1000; 5 6 public ManagerInfo getManagerInfo(String managerName) { 7 String sql = "select a.managerName, a.managerLevelId,a.managerId, a.password" 8 + " from manager a where a.managerName=?"; 9 ManagerInfo manager = jdbcDao.queryForObject(sql, new Object[] { managerName }, ManagerInfo.class); 10 11 return manager; 12 } 13 14 public ManagerToken saveToken(Integer managerId) { 15 ManagerToken managerToken = new ManagerToken(); 16 managerToken.managerId = managerId; 17 18 //生成一个token 19 managerToken.token = TokenGenerator.generateValue(); 20 //过期时间 21 Date expireTime = new Date(System.currentTimeMillis() + EXPIRE); 22 23 // 更新时间/过期时间 24 SimpleDateFormat sm = new SimpleDateFormat("yyyyMMddHHmmss"); 25 Date systemDate = new Date(); 26 managerToken.updateTime = sm.format(systemDate); 27 managerToken.expireTime = sm.format(expireTime); 28 29 String sql = "insert into managertoken (managerId, token, updateTime, expireTime) values (?,?,?,?)" 30 + " ON DUPLICATE KEY UPDATE token=?, updateTime=?, expireTime=?"; 31 jdbcDao.update(sql, new Object[]{managerToken.managerId, managerToken.token, managerToken.updateTime, 32 managerToken.expireTime, managerToken.token, managerToken.updateTime, managerToken.expireTime}); 33 34 return managerToken; 35 } 36 37 @Transactional(propagation= Propagation.REQUIRED, isolation= Isolation.DEFAULT, readOnly = true, rollbackFor = Exception.class) 38 public ManagerInfo getManagerInfo(Integer managerId) { 39 if (managerId == null) { 40 return null; 41 } 42 43 String sql = "select a.managerId, a.managerName, a.managerLevelId from manager a " + 44 "where a.managerId=?"; 45 ManagerInfo manager = jdbcDao.queryForObject(sql, new Object[]{managerId}, ManagerInfo.class); 46 47 return manager; 48 } 49 50 public ManagerToken queryByToken(String token) { 51 if (token == null || "".equals(token)) { 52 return null; 53 } 54 55 String sql = "select managerid managerId, token, expireTime, updateTime from managertoken where token=?"; 56 ManagerToken managerToken = jdbcDao.queryForObject(sql, new Object[]{token}, ManagerToken.class); 57 58 return managerToken; 59 } 60 61 }
这里省略了一些基础的实体类、工具类,详见代码
登陆测试
先登陆获取到token(localhost:8087/login?username=cscscs&password=4297f44b13955235245b2497399d7a93)
这里测试的用户有两个一个admin(超级管理员), 一个是cscscs
测试需要认证的接口
1.不带token访问
2.带token访问
注意是headers中添加参数token
测试不需要认证的接口
在OAuth2Filter中我对,/app 路径下的接口不需要认证
测试角色认证的接口
/require_role, 这个接口需要admin角色才能访问,在OAuth2Realm中我设置了admin用户为超级管理员角色, cscscs用户为test角色
1. cscscs用户访问(选择cscscs用户的token)
2.admin用户访问(选择admin用户的token)
测试权限认证的接口
平时我们可以设置管理员是否有删除,更新记录的权限
/require_permission在控制器中设置了只有拥有update权限的用户才能访问, 在OAuth2Realm中我给了admin用户update的权限, 给了cscscs用户view的权限
1.使用cscscs用户的token访问
2. 使用admin用户访问
结语
1. 希望能给自己帮助、也给别人帮助,有任何疑问或者意见,在下方留言哦
2, 有很多不足可以改进, 如缓存啊, 更准确的权限设置啊, 但他可以帮你构建一个完整可用的JWT
之前看了网上的例子, 理论很好, 例子却没跑通, aaaaa,,,