Json web token (JWT), 是爲了在網絡應用環境間傳遞聲明而執行的一種基於JSON的開放標準((RFC 7519).該token被設計爲緊湊且安全的, 特別適用於分佈式站點的單點登陸(`SSO`)場景。JWT的聲明通常被用來在身份提供者和服務提供者間傳遞被認證的用戶身份信息, 以便於從資源服務器獲取資源,也能夠增長一些額外的其它業務邏輯所必須的聲明信息,該token也可直接被用於認證,也可被加密。
由於JWT是無狀態的,所以服務端沒法控制已經生成的Token失效,是不可控的
1. 認證,這是比較常見的使用場景,只要用戶登陸過一次系統,以後的請求都會包含簽名出來的token,經過token也能夠用來實現單點登陸。
2. 交換信息,經過使用密鑰對來安全的傳送信息,能夠知道發送者是誰、放置消息被篡改。
項目克隆
項目名稱 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;
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>
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 java
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 }
這個裏面能夠配置權限過濾的規則mysql
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 }
這個裏面能夠設置角色、權限和認證信息git
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 }
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 }
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 }
@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); } }
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 }
這裏省略了一些基礎的實體類、工具類,詳見代碼web
先登錄獲取到token(localhost:8087/login?username=cscscs&password=4297f44b13955235245b2497399d7a93)
這裏測試的用戶有兩個一個admin(超級管理員), 一個是cscscs
1.不帶token訪問spring
2.帶token訪問sql
注意是headers中添加參數token數據庫
在OAuth2Filter中我對,/app 路徑下的接口不須要認證apache
/require_role, 這個接口須要admin角色才能訪問,在OAuth2Realm中我設置了admin用戶爲超級管理員角色, cscscs用戶爲test角色json
1. cscscs用戶訪問(選擇cscscs用戶的token)api
2.admin用戶訪問(選擇admin用戶的token)
平時咱們能夠設置管理員是否有刪除,更新記錄的權限
/require_permission在控制器中設置了只有擁有update權限的用戶才能訪問, 在OAuth2Realm中我給了admin用戶update的權限, 給了cscscs用戶view的權限
1.使用cscscs用戶的token訪問
2. 使用admin用戶訪問
1. 但願能給本身幫助、也給別人幫助,有任何疑問或者意見,在下方留言哦
2, 有不少不足能夠改進, 如緩存啊, 更準確的權限設置啊, 但他能夠幫你構建一個完整可用的JWT
以前看了網上的例子, 理論很好, 例子卻沒跑通, aaaaa,,,