簡單描述:最近在處理鑒權這一塊的東西,需求就是用戶登錄需要獲取token,然后攜帶token訪問接口,token認證成功接口才能返回正確的數據,如果訪問接口時候token過期,就采用刷新token刷新令牌(得到新的token和refresh_token),然后在訪問接口返回數據,如果刷新token也過期了,就提示用戶重新登錄。廢話不多說,直接上代碼。源碼在github上
使用 springboot + thymeleaf + mybatis 搭建的
//核心依賴 <!-- spring security + OAuth2 + JWT start -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>1.0.9.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.2.1.RELEASE</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- spring security + OAuth2 + JWT end -->
//核心配置 application.yml文件
#toekn相關配置
token:
config:
#客戶端標識,類比為token的用戶名,我寫的是項目名
clientId: SOJ_DEMO
#客戶端安全碼,類比為token的密碼,我寫的是假郵箱
secret: soj@123
#表示授權模式: password(密碼模式),authorization_code(授權碼模式)
grantTypes: password
#表示權限范圍,該屬性為可選項
scopes: all
#令牌的有效時長,此處為180s/60,時長為2分鍾 設置短一點是為了測試刷新token
accessTokenValidity: 120
#刷新令牌的有效時長
refreshTokenValidity: 36000
#資源ID號
resourceId: SOJ_DEMO
#token簽名的key,用於token對稱加解密
signingKey: SOJ_SYMMETRY
#設置刷新令牌機制.true(重復使用:更新access_token時長后,refresh_toke時長不更新)。false(與true相反)
isRefreshToken: false
利用keytool工具生成密鑰對(非對稱秘鑰 公鑰私鑰) 來執行簽名過程 keytool工具使用幫助文檔 這里得好好看看 最好一步步來 繁瑣的過程,我的媽呀 真的要吐了 太惡心了
keytool -genkeypair -alias jwt -keyalg RSA -keypass xc1234 -keystore jwt.jks -storepass xc1234
執行完之后會在C:\Users\Administrator目錄下生成一個jwt.jks文件, .jks文件包含了我們的秘鑰 公鑰 私鑰,后續會在代碼中讀取此文件中的公鑰私鑰。
把生成的jwt.jks文件放到resources目錄下的certificate文件夾中,這里給一下目錄結構
然后在POM文件中加入對此文件的引入
//pom文件
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
<excludes>
<exclude>certificate/*.jks</exclude>
</excludes>
</resource>
<resource>
<directory>src/main/resources</directory>
<filtering>false</filtering>
<includes>
<include>certificate/*.jks</include>
</includes>
</resource>
</resources>
</build>
下邊開始代碼
JwtToken主要負責token解析和加解密 和上邊的jwt.jks文件打交道 讀取公鑰私鑰

package com.xc.soj_demo.jwt;
import io.jsonwebtoken.ClaimJwtException;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.apache.commons.lang3.StringUtils;
import org.springframework.context.annotation.Configuration;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.Date;
/**
* token加解密
*
* 1.對稱加解密
* 2.非對稱加解密(RSA)
*
*/
@Configuration
public class JwtToken {
/**
* 加載jwt.jks文件
*/
private static InputStream inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("certificate/jwt.jks");
private static PrivateKey privateKey = null;
private static PublicKey publicKey = null;
static {
try {
KeyStore keyStore = KeyStore.getInstance("JKS");
keyStore.load(inputStream, "xc1234".toCharArray());
privateKey = (PrivateKey) keyStore.getKey("jwt", "xc1234".toCharArray());
publicKey = keyStore.getCertificate("jwt").getPublicKey();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 生成jwt token(非對稱加密模式 公鑰私鑰)
*/
public static String generateTokenRSA(String subject, int expirationSeconds) {
Date nowDate = new Date();
Date expireDate = new Date(nowDate.getTime() + expirationSeconds * 1000);
return Jwts.builder()
.setClaims(null)
.setHeaderParam("typ", "JWT")
.setSubject(subject)
.setIssuedAt(nowDate)
.setExpiration(expireDate)
.signWith(SignatureAlgorithm.RS256, privateKey)
.compact();
}
/**
* 解析jwt token(非對稱加密模式 公鑰私鑰)
*/
public static Claims parseTokenRSA(String token) {
if (StringUtils.isEmpty(token)) {
return null;
}
try {
return Jwts.parser()
.setSigningKey(publicKey)
.parseClaimsJws(token)
.getBody();
}catch (Exception e){
System.out.println("try-catch:validate is token error ");
return null;
}
}
/**
* token是否過期
* @return true:過期
*/
public static boolean isTokenExpired(Date expiration) {
boolean before = expiration.before(new Date());
return before;
}
/**
* 生成jwt token(對稱加密模式)
*/
public static String generateToken(String subject, int expirationSeconds,
String signingKey) {
Date nowDate = new Date();
Date expireDate = new Date(nowDate.getTime() + expirationSeconds * 1000);
return Jwts.builder()
.setHeaderParam("typ", "JWT")
.setSubject(subject)
.setIssuedAt(nowDate)
.setExpiration(expireDate)
.signWith(SignatureAlgorithm.HS512, signingKey)
.compact();
}
/**
* 解析jwt token(對稱加密模式)
*/
public static String parseToken(String token,String signingKey) {
if (StringUtils.isEmpty(token)) {
return null;
}
token = StringUtils.substringAfter(token, "bearer");//定義token令牌的類型為bearer
Claims claims;
try {
claims = Jwts.parser().setSigningKey(signingKey.getBytes("UTF-8")).parseClaimsJws(token).getBody();
} catch (ClaimJwtException e) {
//源碼DefaultJwtParser.Class中的處理過程是 從token中取出載荷payload部分,解析出claim(claim中存在用戶信息),然后在解析是否過期,最后才拋出的異常
//所以是可以從 ClaimJwtException e中取出需要的部分 並且源碼ClaimJwtException.Class類中有header和claim兩個私有屬性並提供了get方法
claims = e.getClaims();
} catch (UnsupportedEncodingException e) {
return null;
}
String localUser = (String) claims.get("userinfo");// 拿到當前用戶
return localUser;
}
}
AuthServerConfig主要負責配置令牌加載的屬性,自定義用戶信息到token令牌內

package com.xc.soj_demo.authenticationConfig; import com.alibaba.fastjson.JSON; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken; import org.springframework.security.oauth2.common.OAuth2AccessToken; import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; import org.springframework.security.oauth2.provider.OAuth2Authentication; import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter; import java.util.Collection; import java.util.HashMap; import java.util.Map; /** * OAuth2配置類 * * 1.配置令牌加載的屬性 * 2.自定義用戶信息到token令牌內 * */ @Configuration @EnableAuthorizationServer public class AuthServerConfig extends AuthorizationServerConfigurerAdapter { @Autowired private TokenConfig tokenConfig; /** * 注入authenticationManager * 來支持 password grant type */ @Autowired private AuthenticationManager authenticationManager; /** * 注入userDetailService * 來支持 refresh_token grant type * 人話講 就是toekn失效 需要用到refresh_token去重新請求 /oauth/token來簽發新的token和refresh_token */ @Autowired private UserDetailsService userDetailService; /** * 定義oauth/token類接口信息 * * @description tokenConfig map * map.get("clientId") 類比為token的用戶名 * map.get("secret") 類比為token的密碼 * map.get("grantTypes")表示授權類型 grant_type: password(密碼模式) * map.get("scopes")權限范圍 * map.get("accessTokenValidity")token有效期 * map.get("refreshTokenValidity")刷新token有效時間 * map.get("resourceId")定義資源令牌頭部,資源服務器驗證令牌時用到 * */ @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { Map<String, String> map = tokenConfig.getConfig(); clients.inMemory() .withClient(map.get("clientId")) .secret("{noop}" + map.get("secret")) .authorizedGrantTypes(map.get("grantTypes"), "refresh_token") .scopes(map.get("scopes")) .accessTokenValiditySeconds(Integer.parseInt(map.get("accessTokenValidity"))) .refreshTokenValiditySeconds(Integer.parseInt(map.get("refreshTokenValidity"))) .resourceIds(tokenConfig.getResourceId()) // .authorities("ADMIN") // .redirectUris("http://localhost:8882/login") // 認證成功重定向URL .autoApprove(true);// 自動認證 } /** * token令牌配置 * * @description 1.定義自定義token生成方式、tokenStore、、認證管理器 * 2.定義token加解密轉換器 * 3.定義token請求方式 */ @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints.accessTokenConverter(accessTokenConverter()); endpoints.authenticationManager(authenticationManager); endpoints.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST); endpoints.userDetailsService(userDetailService);//支持refresh_token機制 endpoints.reuseRefreshTokens(tokenConfig.isRefreshToken());//和配置文件對應的 具體看application.yml最后一項 } /** * OAuth2服務配置 * * @description 1.允許/oauth/token被調用,默認deny * 2.允許所有檢查token,默認deny。必須加,否則check_token不能訪問顯示401未授權錯誤 * 3.允許表單認證 */ @Override public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception { oauthServer // .tokenKeyAccess("permitAll()") // .checkTokenAccess("permitAll()") .allowFormAuthenticationForClients(); } /** * 生成jwt令牌 * @return */ @Bean public JwtAccessTokenConverter accessTokenConverter() { JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter() { /*** * 重寫增強token方法,用於自定義一些token總需要封裝的信息 * @return */ @Override public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) { Authentication user = authentication.getUserAuthentication(); String userName = user.getName(); Collection<? extends GrantedAuthority> authority = user.getAuthorities(); // 得到用戶名,去處理數據庫可以拿到當前用戶的信息和角色信息(需要傳遞到服務中用到的信息) final Map<String, Object> additionalInformation = new HashMap<>(); // Map假裝用戶實體 Map<String, Object> userinfo = new HashMap<>(); userinfo.put("userId", "001"); userinfo.put("username", userName); userinfo.put("authOrity", authority); additionalInformation.put("userinfo", JSON.toJSONString(userinfo)); ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInformation); OAuth2AccessToken enhancedToken = super.enhance(accessToken, authentication); return enhancedToken; } }; // 生成簽名的key,資源服務使用相同的字符達到一個對稱加密的效果,生產時候使用RSA非對稱加密方式 accessTokenConverter.setSigningKey(tokenConfig.getSigningKey()); return accessTokenConverter; } }
OauthInterceprtor負責攔截oauth的異常,主要是token過期之后的處理,采用刷新令牌機制重新獲取token,再次請求資源

package com.xc.soj_demo.authenticationConfig; import com.xc.soj_demo.constant.CodeConstant; import com.xc.soj_demo.dao.UserDao; import com.xc.soj_demo.entity.User; import com.xc.soj_demo.jwt.JwtToken; import com.xc.soj_demo.util.JsonUtil; import net.sf.json.JSONObject; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.ResponseEntity; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.provider.error.DefaultWebResponseExceptionTranslator; import org.springframework.security.oauth2.provider.error.OAuth2AuthenticationEntryPoint; import org.springframework.security.oauth2.provider.error.WebResponseExceptionTranslator; import org.springframework.web.client.RestTemplate; import javax.annotation.Resource; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.*; /** * OAuth2異常攔截類 * * 1.對oauth錯誤異常進行攔截,這里主要針對令牌過期進行處理 * 2.新的令牌與刷新令牌的存儲 * 3.載入用戶信息到spring Security的ContextHolder中,保證后續url轉發 * 4.刷新令牌過期后的返回狀態 * */ public class OauthInterceptor extends OAuth2AuthenticationEntryPoint { @Value("${server.port}") private String port; @Autowired private TokenConfig tokenConfig; /** * 在啟動類中注入了restTemplate Bean */ @Autowired RestTemplate restTemplate; @Resource private UserDao dao; @Autowired private AuthenticationManager authenticationManager; private WebResponseExceptionTranslator exceptionTranslator = new DefaultWebResponseExceptionTranslator(); @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { try { ResponseEntity<?> result = exceptionTranslator.translate(authException); JSONObject objBody = JSONObject.fromObject(result.getBody()); String message = objBody.getString("message"); //判斷是否為"訪問令牌過期",如果不是則以默認的方法繼續處理其他異常 if (message.contains("Access token expired")) { //根據訪問令牌,解析出當前令牌用戶的用戶名稱,密碼等信息 String localUser = JwtToken.parseToken(request.getHeader("Authorization"), tokenConfig.getSigningKey()); @SuppressWarnings("unchecked") Map<String, Object> userMap = (Map<String, Object>) JsonUtil.json2Map(localUser); String username = (String)userMap.get("username"); //根據用戶名稱,從數據庫獲取用戶的刷新令牌 String refresh_token = dao.getRefreshToken(username); //獲取當前用戶信息 User userObj = dao.getUserByUserName(username); Map<String, Object> map = new HashMap<>(); map.put("code", 1);//用戶存在 密碼正確 map.put("userId", userObj.getUserId()); map.put("username", userObj.getUsername()); map.put("password", userObj.getPassword()); List<String> listPermission = new ArrayList<>(); listPermission.add("user::add"); listPermission.add("user::list"); listPermission.add("user::update"); listPermission.add("user::delete"); List<String> listRole = new ArrayList<>(); // listRole.add("sys_admin"); // listRole.add("admin"); listRole.add(userObj.getUserRole()); map.put("authOrity", listRole); map.put("userPermission", listPermission); //獲取OAuth2框架的配置信息,用於訪問刷新令牌接口 Map<String, String> tokenMap = tokenConfig.getConfig(); Map<String,String> mapParam = new HashMap<>(); mapParam.put("username", userObj.getUsername()); mapParam.put("password", userObj.getPassword()); mapParam.put("client_id", tokenMap.get("clientId")); mapParam.put("client_secret", tokenMap.get("secret")); mapParam.put("grant_type", "refresh_token");//這里沒有寫錯 采用刷新令牌的方式 mapParam.put("refresh_token", refresh_token); try { @SuppressWarnings("unchecked") Map<String, String> mapResult = restTemplate .getForObject( "http://localhost:"+port+"/oauth/token?username={username}&password={password}&client_id={client_id}&client_secret={client_secret}&grant_type={grant_type}&refresh_token={refresh_token}", Map.class, mapParam); // 如果刷新成功 跳轉到原來需要訪問的頁面 //寫入用戶信息到公共變量中,寫入信息到SecurityContext中 CodeConstant.USER_MAP = map; List<GrantedAuthority> grantedAuthorityList = new ArrayList<>(); for (String role : listRole) { grantedAuthorityList.add(new SimpleGrantedAuthority( role)); } UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken( userObj.getUsername(), userObj.getPassword(), grantedAuthorityList); Authentication authentications = authenticationManager .authenticate(authRequest); SecurityContextHolder.getContext().setAuthentication( authentications); response.setHeader("access_token", mapResult.get("access_token")); // response.setHeader("refresh_token", // mapResult.get("refresh_token")); //把新獲取到的refresh_token存到數據庫 dao.setRefreshToken(userObj.getUserId(),mapResult.get("access_token")); response.setHeader("isRefreshToken", "yes"); request.getRequestDispatcher(request.getRequestURI()) .forward(request, response); } catch (Exception e) { // e.printStackTrace(); // 獲取刷新令牌失敗時(刷新令牌過期時),返回指定格式的錯誤信息 response.setHeader("Content-Type", "application/json;charset=utf-8"); response.getWriter().print("{\"code\":411,\"message\":\"刷新令牌以過期,需要重新登錄.\"}"); response.getWriter().flush(); } }else{ super.commence(request,response,authException); } } catch (Exception e) { e.printStackTrace(); } } }
ResourceConfiguration資源服務器配置,具體負責后台 哪些接口放開,哪些接口攔截

package com.xc.soj_demo.authenticationConfig;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
/**
* 資源服務器-配置類
*
* 1.設置接口訪問權限
* 2.token驗證
*
*/
@Configuration
@EnableResourceServer
public class ResourceConfiguration extends ResourceServerConfigurerAdapter {
@Value("${token.resourceId}")
private String resourceId;
/**
* 定義資源服務器接口訪問權限
*
* @description 1.定義無權限接口
* 2.定義接口訪問權限為admin
* 3.定義接口訪問權限為sys_admin
*/
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/order/*","/getToken","/parseToken","/sys/test","/sys/login","/sys/doLogin","/js/**").permitAll()// "/order/*"資源是開放的
.and().authorizeRequests()
.antMatchers(HttpMethod.OPTIONS).permitAll()
// .antMatchers("/B").hasRole("admin")
// .antMatchers("/admin").hasRole("sys_admin")
.anyRequest().authenticated();
}
/**
* 定義資源服務器解析協議表頭(需要與認證服務器定義的表頭一致)
*/
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.resourceId(resourceId).stateless(true);
resources.authenticationEntryPoint(new OauthInterceptor());
}
}
TokenConfig從配置文件讀取token的相關配置

package com.xc.soj_demo.authenticationConfig; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; import java.util.HashMap; import java.util.Map; @Component @Data @ConfigurationProperties(prefix="token") public class TokenConfig { private Map<String, String> config = new HashMap<>(); private String resourceId; private String signingKey; private boolean isRefreshToken; }
UserDetailService 用戶信息獲取

package com.xc.soj_demo.authenticationConfig; import com.xc.soj_demo.constant.CodeConstant; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Component; import java.util.ArrayList; import java.util.List; import java.util.Map; /** * 用戶信息獲取(用戶名稱,密碼,權限) * */ @Component public class UserDetailService implements UserDetailsService { @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { //這個地方可以通過username從數據庫獲取正確的用戶信息,包括密碼和權限等。 // 從user獲取正確的用戶信息,包括密碼和權限等。 Map<String, Object> user = CodeConstant.USER_MAP; if (user != null) { @SuppressWarnings("unchecked") List<String> authOrity = (List<String>) user.get("authOrity"); String PASSWORD = "{noop}" + user.get("password").toString(); List<GrantedAuthority> grantedAuthorityList = new ArrayList<>(); for (String auth : authOrity) { grantedAuthorityList.add(new SimpleGrantedAuthority(auth)); } return new User(username, PASSWORD, grantedAuthorityList); } else { throw new UsernameNotFoundException("用戶[" + username + "]不存在"); } } }
WebSecurityConfig認證服務器配置

package com.xc.soj_demo.authenticationConfig;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
/**
* Security-配置類(認證服務器)
*
* 1.配置請求URL的訪問策略
* 2.自定義認證登錄頁面URL
* 3.配置OAuth2密碼模式
*
*/
@EnableWebSecurity//開啟權限驗證
@EnableGlobalMethodSecurity(prePostEnabled = true, proxyTargetClass = true)//通過表達式控制方法權限
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 配置訪問策略
*
* @description 1.設置授權請求
* 2.自定義登錄界面
* 3.設置使用jwt,可以允許跨域
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.requestMatchers()
.antMatchers("/login")
.antMatchers("/oauth/**")
.and().authorizeRequests()
.anyRequest().authenticated()
.and().formLogin().loginPage("/login").permitAll()
.and().csrf().disable();
}
/**
* 需要配置這個支持password模式 support password grant type
* @return
* @throws Exception
*/
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
核心代碼到這里就結束了,下邊是業務代碼,和上邊的關聯起來 MVC那一套
常量類:
package com.xc.soj_demo.constant; import java.util.Map; public class CodeConstant { //為了減少查詢數據庫的次數,把用戶的一些信息暫時存放到這里 public static Map<String, Object> USER_MAP = null; }
Controller:

package com.xc.soj_demo.controller; import com.xc.soj_demo.entity.User; import com.xc.soj_demo.service.UserService; import com.xc.soj_demo.util.JsonUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Controller; import org.springframework.ui.ModelMap; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import javax.annotation.Resource; import java.util.ArrayList; import java.util.List; import java.util.Map; @Controller @RequestMapping("/sys") public class LoginController { private static final Logger logger = LoggerFactory.getLogger(LoginController.class); @Resource private UserService userService; //測試頁面 @RequestMapping("/test") public String testThymeleaf(ModelMap model) { User user = new User(); user.setUsername("蓋聶"); user.setUserRole("大叔"); model.addAttribute("user", user); return "/viewTest"; } //登錄頁面 @RequestMapping("/login") public String login(ModelMap model) { return "/login"; } @RequestMapping(value = "/doLogin") @ResponseBody public String login(User user) { Map<String, Object> resultMap = userService.login(user.getUsername(), user.getPassword()); String str = JsonUtil.map2Json(resultMap); logger.info(str); return str; } //測試頁面 @RequestMapping("/getList") @ResponseBody public List<String> getList() { List<String> list = new ArrayList<>(); list.add("aa"); list.add("bb"); return list; } }
Dao:
package com.xc.soj_demo.dao; import org.apache.ibatis.annotations.Mapper; import com.xc.soj_demo.entity.User; import java.util.Map; @Mapper public interface UserDao { User getUserByUserName(String username); void addToken2User(Map<String, Object> map); void setRefreshToken(String userId, String refreshToken); String getPasswordByUserName(String username); String getRefreshToken(String username)throws Exception; String selectRefreshTokenByUserId(Integer userId); }
實體類:

package com.xc.soj_demo.entity;
import lombok.Data;
@Data
public class User {
private String userId;
private String username;
private String password;
private String userPermission;
private String userRole;
private String token;
private String refreshToken;
}
實現類:

package com.xc.soj_demo.service.impl; import com.fasterxml.jackson.core.JsonProcessingException; import com.xc.soj_demo.authenticationConfig.TokenConfig; import com.xc.soj_demo.constant.CodeConstant; import com.xc.soj_demo.dao.UserDao; import com.xc.soj_demo.entity.User; import com.xc.soj_demo.service.UserService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.stereotype.Service; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.client.RestTemplate; import javax.servlet.http.HttpServletRequest; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @Service("userService") public class UserServiceImpl implements UserService { private static final Logger logger = LoggerFactory.getLogger(UserServiceImpl.class); @Value("${server.port}") private String port; @Autowired private UserDao dao; @Autowired HttpServletRequest request; @Autowired RestTemplate restTemplate; @Autowired private TokenConfig tokenConfig; @Override public Map<String, Object> login(String username, String password) { Map<String, Object> map = new HashMap<>(); User userObj = dao.getUserByUserName(username); // 判斷用戶是否存在 if (null == userObj) { map.put("code", 1);// 用戶不存在 return map; } // 判斷密碼是否正確 if (!password.equals(userObj.getPassword())) { map.put("code", -1);// 密碼錯誤 return map; } map.put("code", 0);// 用戶存在 密碼正確 map.put("userId", userObj.getUserId()); map.put("username", userObj.getUsername()); String userId = userObj.getUserId(); // List<String> listAuthority = dao.getUserPermissionByUserid(userId); //模擬 查詢用戶權限(查詢結果 可以對用戶進行增刪改查) List<String> listPermission = new ArrayList<>(); listPermission.add("user::add"); listPermission.add("user::list"); listPermission.add("user::update"); listPermission.add("user::delete"); // List<String> listRole = dao.getUserRolesByUid(userId); //模擬 查詢用戶角色 (查詢結果 userObj具有系統管理員 普通管理員的角色) List<String> listRole = new ArrayList<>(); // listRole.add("sys_admin"); // listRole.add("admin"); listRole.add(userObj.getUserRole()); map.put("authOrity", listRole); map.put("userPermission", listPermission); map.put("password", password); try { map.put("access_token", getOAuthToken(map)); } catch (JsonProcessingException e) { e.printStackTrace(); } return map; } /** * 調用OAuth2的獲取令牌接口 * * @description 1.將用戶信息存入公共map中 2.獲取訪問令牌 3.寫入"刷新令牌"到數據庫 * */ private String getOAuthToken(Map<String, Object> map) throws JsonProcessingException { CodeConstant.USER_MAP = map; Map<String, String> tokenMap = tokenConfig.getConfig(); MultiValueMap<String, String> formData = new LinkedMultiValueMap<String, String>(); formData.add("username", map.get("username").toString()); formData.add("password", map.get("password").toString()); formData.add("client_id", tokenMap.get("clientId")); formData.add("client_secret", tokenMap.get("secret")); formData.add("grant_type", tokenMap.get("grantTypes")); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); String urlStr = "http://localhost:" + port + "/oauth/token"; Map<?, ?> resultMap = restTemplate.exchange(urlStr, HttpMethod.POST, new HttpEntity<MultiValueMap<String, String>>(formData, headers), Map.class).getBody(); if (null != resultMap) { try { setRefreshToken(map.get("userId").toString(), resultMap.get("refresh_token").toString()); } catch (Exception e) { e.printStackTrace(); } return resultMap.get("access_token").toString(); } return null; } /** * 更新用戶的刷新令牌 * */ public void setRefreshToken(String userId, String refreshToken) throws Exception { dao.setRefreshToken(userId, refreshToken); } public String getPasswordByUserName(String username) throws Exception { String password = dao.getPasswordByUserName(username); return password; } /** * @Description 根據用戶ID獲取刷新token * @param userId * @param refreshToken * @return Boolean * @author chao.song */ public Boolean verdictRefreshTokenByUId(Integer userId, String refreshToken) { if (userId == null || userId < 0) { return false; } if (refreshToken.isEmpty()) { return false; } String baseRefreshToken = dao.selectRefreshTokenByUserId(userId); if (refreshToken.equals(baseRefreshToken)) { return true; } return false; } }
Json工具類

package com.xc.soj_demo.util; import com.fasterxml.jackson.core.JsonParser.Feature; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.ObjectMapper; import java.io.IOException; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; public class JsonUtil { private static final ObjectMapper mObjectMapper = new ObjectMapper(); static { mObjectMapper.configure(Feature.ALLOW_BACKSLASH_ESCAPING_ANY_CHARACTER, true); mObjectMapper.configure(Feature.ALLOW_UNQUOTED_CONTROL_CHARS, true); mObjectMapper.configure(Feature.ALLOW_UNQUOTED_FIELD_NAMES, true); mObjectMapper.configure(Feature.ALLOW_SINGLE_QUOTES, true); mObjectMapper.configure(Feature.ALLOW_NUMERIC_LEADING_ZEROS, true); mObjectMapper.configure(Feature.ALLOW_NON_NUMERIC_NUMBERS, true); mObjectMapper.configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true); mObjectMapper.configure(DeserializationFeature.READ_ENUMS_USING_TO_STRING, true); mObjectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true); mObjectMapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true); mObjectMapper.configure(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS, true); DateFormat myDateFormat = new SimpleDateFormat("yyyy-MM-DD hh:mm:ss"); mObjectMapper.getSerializationConfig().with(myDateFormat); mObjectMapper.getDeserializationConfig().with(myDateFormat); } /** * parameters key */ private static final String PARA_KEY = "parameters"; /** * @param jsonString * @return */ public static Map<?, ?> json2Map(String jsonString) { try { Map<?, ?> map = mObjectMapper.readValue(jsonString, Map.class); return map; } catch (JsonMappingException e) { e.printStackTrace(); return null; } catch (IOException e) { e.printStackTrace(); return null; } } /** * @param jsonString * @return * @description */ public static List<Map<?, ?>> json2MapOfArrayList(String jsonString) { try { JavaType javaType = getCollectionType(ArrayList.class, Map.class); @SuppressWarnings("unchecked") List<Map<?, ?>> arrayList = (List<Map<?, ?>>) mObjectMapper.readValue(jsonString, javaType); return arrayList; } catch (JsonMappingException e) { e.printStackTrace(); return null; } catch (IOException e) { e.printStackTrace(); return null; } } /** * @param collectionClass * @param elementClasses * @return */ public static JavaType getCollectionType(Class<?> collectionClass, Class<?>... elementClasses) { return mObjectMapper.getTypeFactory().constructParametricType(collectionClass, elementClasses); } /** * @param map * @return * @description map to json string */ public static String map2Json(Map<?, ?> map) { try { String ret = ""; ret = mObjectMapper.writeValueAsString(map); // remove all "\" ret = ret.replaceAll("\\\\", ""); if (ret.contains("\"[")) { ret = ret.replaceAll("\"\\[", "\\["); } if (ret.contains("]\"")) { ret = ret.replaceAll("\\]\"", "\\]"); } if (ret.contains("\"{")) { ret = ret.replaceAll("\"\\{", "\\{"); } if (ret.contains("}\"")) { ret = ret.replaceAll("\\}\"", "\\}"); } return ret; } catch (Exception e) { e.printStackTrace(); return null; } } /** * @param list * @return */ public static String listMap2Json(List<Map<String, Object>> list) { try { String ret = ""; ret = mObjectMapper.writeValueAsString(list); // remove all "\" ret = ret.replaceAll("\\\\", ""); return ret; } catch (Exception e) { e.printStackTrace(); return null; } } /** * @param strContent * @return * @description */ public static String getJsonString(String strContent) { String ret = ""; if (!strContent.contains(PARA_KEY)) { return ret; } String regex = "parameters[\\s]+="; Pattern p = Pattern.compile(regex); Matcher m = p.matcher(strContent); if (m.find()) { strContent = strContent.replaceFirst(regex, ""); } else { regex = "parameters="; p = Pattern.compile(regex); m = p.matcher(strContent); if (m.find()) { strContent = strContent.replaceFirst(regex, ""); } } strContent = strContent.trim(); return strContent; } /** * @param byteArray * @return * @description */ public static String bytes2Hex(byte[] byteArray) { StringBuffer strBuf = new StringBuffer(); for (int i = 0; i < byteArray.length; i++) { if (byteArray[i] >= 0 && byteArray[i] < 16) { strBuf.append("0"); } strBuf.append(Integer.toHexString(byteArray[i] & 0xFF)); } return strBuf.toString(); } /** * method_name: mapFormatString2List * <p> * parameters: mapString format is: [ { id=1, time=2013-11-09 09:00:00 }, { * id=2, time=2013-11-10 09:00:00 } ] * <p> * <p> * return: List<Map<String, Object>> */ public static List<Map<String, Object>> mapFormatString2List(String strContent) { List<Map<String, Object>> list = new ArrayList<Map<String, Object>>(); String ret = ""; String regex = "\\{[^}]+\\}"; // \\{[^}]+\\} {[^}]*} Pattern p = Pattern.compile(regex); Matcher m = p.matcher(strContent); while (m.find()) { ret = m.group(); ret = ret.replaceAll("\\{", ""); ret = ret.replaceAll("\\}", ""); ret = ret.trim(); Map<String, Object> map = transStringToMap(ret); list.add(map); } return list; } /** * method_name: transStringToMap * <p> * parameters: mapString format is: id=1, time=2013-11-09 09:00:00 (delim:",", * token:"=") * <p> * return: Map */ public static Map<String, Object> transStringToMap(String mapString) { Map<String, Object> map = new HashMap<String, Object>(); StringTokenizer items; for (StringTokenizer entrys = new StringTokenizer(mapString, ","); entrys.hasMoreTokens(); map .put(items.nextToken().trim(), items.hasMoreTokens() ? ((Object) (items.nextToken().trim())) : null)) { items = new StringTokenizer(entrys.nextToken(), "="); } return map; } /** * @param str * @return * @description trim */ public static String trimAll(String str) { if (null == str || str.length() <= 0) { return str; } else { return str.replaceAll("^[ ]+|[ ]+$", ""); } } }
mapper文件

<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.xc.soj_demo.dao.UserDao"> <resultMap id="UserResultMap" type="com.xc.soj_demo.entity.User"> <id property="userId" column="user_id"/> <result property="username" column="username"/> <result property="password" column="password"/> <result property="userPermission" column="user_permission"/> <result property="userRole" column="user_role"/> <result property="token" column="token"/> <result property="refreshToken" column="refresh_token" /> </resultMap> <sql id="tableName">t_user</sql> <update id="addToken2User" parameterType="java.util.Map"> update <include refid="tableName"/> set token = #{token} where user_id = #{userId} </update> <select id="getUserByUserName" parameterType="String" resultMap="UserResultMap"> select * from <include refid="tableName"/> where username = #{username} </select> <!-- 獲取用戶密碼 --> <select id="getPasswordByUserName" parameterType="String" resultType="String"> select password from <include refid="tableName" /> where username = #{username} </select> <!-- 更新刷新令牌 --> <update id="setRefreshToken"> update <include refid="tableName" /> set refresh_token = #{refreshToken} where user_id = #{userId} </update> <!-- 獲取用戶的刷新令牌 --> <select id="getRefreshToken" parameterType="String" resultType="String"> select refresh_token from <include refid="tableName" /> where username = #{username} </select> <select id="selectRefreshTokenByUserId" parameterType="Integer" resultType="String"> select refresh_token from <include refid="tableName" /> where user_id = #{userId} </select> </mapper>
記得在主啟動類中把restTemplate注入進去,這個是要用到的
測試:
首先登陸獲取token
拿到token后請求后台接口getList
然后 等2分鍾 等token過期 再次請求getList
可以看到 第一次使用token去請求的時間是 15:21:26 等token過期之后再請求的時間是15:24:32 在配置文件中配置的token有效期是2分鍾 刷新token時間長一點是10個小時,這時候請求也可以訪問的原因 后台的刷新tokne機制起了作用。請求接口的時候使用的token是紫色方框中的那個后台驗證 這個token已過期 然后刷新token機制開始工作。從已失效的tokne的載荷中 取出用戶名,然后根據用戶名查詢用戶信息,得到refresh_token,然后使用refresh_token請求/oauth/token 獲取新的tokne也就是綠色方框中的access_token,最后在接着訪問接口的url的到結果返回回來。刷新token機制 具體體現在OauthInterceptor.java中