單點登入SSO方法之SpringSecurity+JWT實現

透過前面幾篇文章我們詳細的介紹了

SpringSecurity

的使用,本文我們來看下,結合

JWT

來實現

單點登入

操作。

一、什麼是單點登陸

單點登入

(Single Sign On),簡稱為

SSO

,是目前比較流行的企業業務整合的解決方案之一。SSO的定義是在多個應用系統中,使用者只需要登入一次就可以訪問所有相互信任的應用系統

二、簡單的執行機制

單點登入的機制其實是比較簡單的,用一個現實中的例子做比較。某公園內部有許多獨立的景點,遊客可以在各個景點門口單獨買票。對於需要遊玩所有的景點的遊客,這種買票方式很不方便,需要在每個景點門口排隊買票,錢包拿 進拿出的,容易丟失,很不安全。於是絕大多數遊客選擇在大門口買一張通票(也叫套票),就可以玩遍所有的景點而不需要重新再買票。他們只需要在每個景點門 口出示一下剛才買的套票就能夠被允許進入每個獨立的景點。

單點登入的機制也一樣,如下圖所示,

單點登入SSO方法之SpringSecurity+JWT實現

使用者認證

:這一環節主要是使用者向認證伺服器發起認證請求,認證伺服器給使用者返回一個成功的令牌token,主要在認證伺服器中完成,即圖中的認證系統,注意認證系統只能有一個。

身份校驗

:這一環節是使用者攜帶token去訪問其他伺服器時,在其他伺服器中要對token的真偽進行檢驗,主要在資源伺服器中完成,即圖中的應用系統2 3

三、JWT介紹

概念說明

從分散式認證流程中,我們不難發現,這中間起最關鍵作用的就是token,token的安全與否,直接關係到系統的健壯性,這裡我們選擇使用

JWT

來實現token的生成和校驗。

JWT

,全稱

JSON Web Token

,官網地址https://jwt。io,是一款出色的分散式身份校驗方案。可以生成token,也可以解析檢驗token。

JWT生成的token由三部分組成:

頭部

:主要設定一些規範資訊,簽名部分的編碼格式就在頭部中宣告。

載荷

:token中存放有效資訊的部分,比如使用者名稱,使用者角色,過期時間等,但是不要放密碼,會洩露!

簽名

:將頭部與載荷分別採用base64編碼後,用“。”相連,再加入鹽,最後使用頭部宣告的編碼型別進行編碼,就得到了簽名。

JWT生成token的安全性分析

從JWT生成的token組成上來看,要想避免token被偽造,主要就得看簽名部分了,而簽名部分又有三部分組成,其中頭部和載荷的base64編碼,幾乎是透明的,毫無安全性可言,那麼最終守護token安全的重擔就落在了加入的

上面了!試想:如果生成token所用的鹽與解析token時加入的鹽是一樣的。豈不是類似於中國人民銀行把人民幣防偽技術公開了?大家可以用這個鹽來解析token,就能用來偽造token。這時,我們就需要對鹽採用

非對稱加密

的方式進行加密,以達到生成token與校驗token方所用的鹽不一致的安全效果!

非對稱加密RSA介紹

基本原理

:同時生成兩把金鑰:私鑰和公鑰,私鑰隱秘儲存,公鑰可以下發給信任客戶端

私鑰加密

,持有私鑰或公鑰才可以解密

公鑰加密

,持有私鑰才可解密

優點:安全,難以破解

缺點:演算法比較耗時,為了安全,可以接受

歷史:三位數學家Rivest、Shamir 和 Adleman 設計了一種演算法,可以實現非對稱加密。這種演算法用他們三個人的名字縮寫:RSA。

四、SpringSecurity整合JWT

1。認證思路分析

SpringSecurity主要是透過過濾器來實現功能的!我們要找到SpringSecurity實現認證和校驗身份的過濾器!

回顧集中式認證流程

使用者認證

使用

UsernamePasswordAuthenticationFilter

過濾器中

attemptAuthentication

方法實現認證功能,該過濾器父類中

successfulAuthentication

方法實現認證成功後的操作。

身份校驗

使用

BasicAuthenticationFilter

過濾器中

doFilterInternal

方法驗證是否登入,以決定能否進入後續過濾器。

分析分散式認證流程

使用者認證

由於分散式專案,多數是前後端分離的架構設計,我們要滿足可以接受非同步post的認證請求引數,需要修改UsernamePasswordAuthenticationFilter過濾器中attemptAuthentication方法,讓其能夠接收請求體。

另外,預設successfulAuthentication方法在認證通過後,是把使用者資訊直接放入session就完事了,現在我們需要修改這個方法,在認證通過後生成token並返回給使用者。

身份校驗

原來BasicAuthenticationFilter過濾器中doFilterInternal方法校驗使用者是否登入,就是看session中是否有使用者資訊,我們要修改為,驗證使用者攜帶的token是否合法,並解析出使用者資訊,交給SpringSecurity,以便於後續的授權功能可以正常使用。

2。具體實現

為了演示單點登入的效果,我們設計如下專案結構

單點登入SSO方法之SpringSecurity+JWT實現

2。1父工程建立

因為本案例需要建立多個系統,所以我們使用maven聚合工程來實現,首先建立一個父工程,匯入springboot的父依賴即可

org。springframework。boot spring-boot-starter-parent 2。1。3。RELEASE 123456

2。2公共工程建立

然後建立一個common工程,其他工程依賴此係統

單點登入SSO方法之SpringSecurity+JWT實現

匯入JWT相關的依賴

io。jsonwebtoken jjwt-api 0。10。7 io。jsonwebtoken jjwt-impl 0。10。7 runtime io。jsonwebtoken jjwt-jackson 0。10。7 runtime <!——jackson包——> com。fasterxml。jackson。core jackson-databind 2。9。9 <!——日誌包——> org。springframework。boot spring-boot-starter-logging joda-time joda-time org。projectlombok lombok org。springframework。boot spring-boot-starter-test 123456789101112131415161718192021222324252627282930313233343536373839404142

建立相關的工具類

單點登入SSO方法之SpringSecurity+JWT實現

Payload

/** * @program: springboot-54-security-jwt-demo * @description: * @author: 波波烤鴨 * @create: 2019-12-03 10:28 */@Datapublic class Payload { private String id; private T userInfo; private Date expiration;}123456789101112

JsonUtils

package com。dpb。utils;import com。fasterxml。jackson。core。JsonProcessingException;import com。fasterxml。jackson。core。type。TypeReference;import com。fasterxml。jackson。databind。ObjectMapper;import org。slf4j。Logger;import org。slf4j。LoggerFactory;import java。io。IOException;import java。util。List;import java。util。Map;/** * @author: 波波烤鴨 **/public class JsonUtils { public static final ObjectMapper mapper = new ObjectMapper(); private static final Logger logger = LoggerFactory。getLogger(JsonUtils。class); public static String toString(Object obj) { if (obj == null) { return null; } if (obj。getClass() == String。class) { return (String) obj; } try { return mapper。writeValueAsString(obj); } catch (JsonProcessingException e) { logger。error(“json序列化出錯:” + obj, e); return null; } } public static T toBean(String json, Class tClass) { try { return mapper。readValue(json, tClass); } catch (IOException e) { logger。error(“json解析出錯:” + json, e); return null; } } public static List toList(String json, Class eClass) { try { return mapper。readValue(json, mapper。getTypeFactory()。constructCollectionType(List。class, eClass)); } catch (IOException e) { logger。error(“json解析出錯:” + json, e); return null; } } public static Map toMap(String json, Class kClass, Class vClass) { try { return mapper。readValue(json, mapper。getTypeFactory()。constructMapType(Map。class, kClass, vClass)); } catch (IOException e) { logger。error(“json解析出錯:” + json, e); return null; } } public static T nativeRead(String json, TypeReference type) { try { return mapper。readValue(json, type); } catch (IOException e) { logger。error(“json解析出錯:” + json, e); return null; } }}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172

JwtUtils

package com。dpb。utils;import com。dpb。domain。Payload;import io。jsonwebtoken。Claims;import io。jsonwebtoken。Jws;import io。jsonwebtoken。Jwts;import io。jsonwebtoken。SignatureAlgorithm;import org。joda。time。DateTime;import java。security。PrivateKey;import java。security。PublicKey;import java。util。Base64;import java。util。UUID;/** * @author: 波波烤鴨 * 生成token以及校驗token相關方法 */public class JwtUtils { private static final String JWT_PAYLOAD_USER_KEY = “user”; /** * 私鑰加密token * * @param userInfo 載荷中的資料 * @param privateKey 私鑰 * @param expire 過期時間,單位分鐘 * @return JWT */ public static String generateTokenExpireInMinutes(Object userInfo, PrivateKey privateKey, int expire) { return Jwts。builder() 。claim(JWT_PAYLOAD_USER_KEY, JsonUtils。toString(userInfo)) 。setId(createJTI()) 。setExpiration(DateTime。now()。plusMinutes(expire)。toDate()) 。signWith(privateKey, SignatureAlgorithm。RS256) 。compact(); } /** * 私鑰加密token * * @param userInfo 載荷中的資料 * @param privateKey 私鑰 * @param expire 過期時間,單位秒 * @return JWT */ public static String generateTokenExpireInSeconds(Object userInfo, PrivateKey privateKey, int expire) { return Jwts。builder() 。claim(JWT_PAYLOAD_USER_KEY, JsonUtils。toString(userInfo)) 。setId(createJTI()) 。setExpiration(DateTime。now()。plusSeconds(expire)。toDate()) 。signWith(privateKey, SignatureAlgorithm。RS256) 。compact(); } /** * 公鑰解析token * * @param token 使用者請求中的token * @param publicKey 公鑰 * @return Jws */ private static Jws parserToken(String token, PublicKey publicKey) { return Jwts。parser()。setSigningKey(publicKey)。parseClaimsJws(token); } private static String createJTI() { return new String(Base64。getEncoder()。encode(UUID。randomUUID()。toString()。getBytes())); } /** * 獲取token中的使用者資訊 * * @param token 使用者請求中的令牌 * @param publicKey 公鑰 * @return 使用者資訊 */ public static Payload getInfoFromToken(String token, PublicKey publicKey, Class userType) { Jws claimsJws = parserToken(token, publicKey); Claims body = claimsJws。getBody(); Payload claims = new Payload<>(); claims。setId(body。getId()); claims。setUserInfo(JsonUtils。toBean(body。get(JWT_PAYLOAD_USER_KEY)。toString(), userType)); claims。setExpiration(body。getExpiration()); return claims; } /** * 獲取token中的載荷資訊 * * @param token 使用者請求中的令牌 * @param publicKey 公鑰 * @return 使用者資訊 */ public static Payload getInfoFromToken(String token, PublicKey publicKey) { Jws claimsJws = parserToken(token, publicKey); Claims body = claimsJws。getBody(); Payload claims = new Payload<>(); claims。setId(body。getId()); claims。setExpiration(body。getExpiration()); return claims; }}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104

RsaUtils

package com。dpb。utils;import java。io。File;import java。io。IOException;import java。nio。file。Files;import java。security。*;import java。security。spec。InvalidKeySpecException;import java。security。spec。PKCS8EncodedKeySpec;import java。security。spec。X509EncodedKeySpec;import java。util。Base64;/** * @author 波波烤鴨 */public class RsaUtils { private static final int DEFAULT_KEY_SIZE = 2048; /** * 從檔案中讀取公鑰 * * @param filename 公鑰儲存路徑,相對於classpath * @return 公鑰物件 * @throws Exception */ public static PublicKey getPublicKey(String filename) throws Exception { byte[] bytes = readFile(filename); return getPublicKey(bytes); } /** * 從檔案中讀取金鑰 * * @param filename 私鑰儲存路徑,相對於classpath * @return 私鑰物件 * @throws Exception */ public static PrivateKey getPrivateKey(String filename) throws Exception { byte[] bytes = readFile(filename); return getPrivateKey(bytes); } /** * 獲取公鑰 * * @param bytes 公鑰的位元組形式 * @return * @throws Exception */ private static PublicKey getPublicKey(byte[] bytes) throws Exception { bytes = Base64。getDecoder()。decode(bytes); X509EncodedKeySpec spec = new X509EncodedKeySpec(bytes); KeyFactory factory = KeyFactory。getInstance(“RSA”); return factory。generatePublic(spec); } /** * 獲取金鑰 * * @param bytes 私鑰的位元組形式 * @return * @throws Exception */ private static PrivateKey getPrivateKey(byte[] bytes) throws NoSuchAlgorithmException, InvalidKeySpecException { bytes = Base64。getDecoder()。decode(bytes); PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(bytes); KeyFactory factory = KeyFactory。getInstance(“RSA”); return factory。generatePrivate(spec); } /** * 根據密文,生存rsa公鑰和私鑰,並寫入指定檔案 * * @param publicKeyFilename 公鑰檔案路徑 * @param privateKeyFilename 私鑰檔案路徑 * @param secret 生成金鑰的密文 */ public static void generateKey(String publicKeyFilename, String privateKeyFilename, String secret, int keySize) throws Exception { KeyPairGenerator keyPairGenerator = KeyPairGenerator。getInstance(“RSA”); SecureRandom secureRandom = new SecureRandom(secret。getBytes()); keyPairGenerator。initialize(Math。max(keySize, DEFAULT_KEY_SIZE), secureRandom); KeyPair keyPair = keyPairGenerator。genKeyPair(); // 獲取公鑰並寫出 byte[] publicKeyBytes = keyPair。getPublic()。getEncoded(); publicKeyBytes = Base64。getEncoder()。encode(publicKeyBytes); writeFile(publicKeyFilename, publicKeyBytes); // 獲取私鑰並寫出 byte[] privateKeyBytes = keyPair。getPrivate()。getEncoded(); privateKeyBytes = Base64。getEncoder()。encode(privateKeyBytes); writeFile(privateKeyFilename, privateKeyBytes); } private static byte[] readFile(String fileName) throws Exception { return Files。readAllBytes(new File(fileName)。toPath()); } private static void writeFile(String destPath, byte[] bytes) throws IOException { File dest = new File(destPath); if (!dest。exists()) { dest。createNewFile(); } Files。write(dest。toPath(), bytes); }}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103

在通用子模組中編寫測試類生成rsa公鑰和私鑰

/** * @program: springboot-54-security-jwt-demo * @description: * @author: 波波烤鴨 * @create: 2019-12-03 11:08 */public class JwtTest { private String privateKey = “c:/tools/auth_key/id_key_rsa”; private String publicKey = “c:/tools/auth_key/id_key_rsa。pub”; @Test public void test1() throws Exception{ RsaUtils。generateKey(publicKey,privateKey,“dpb”,1024); }}1234567891011121314151617

單點登入SSO方法之SpringSecurity+JWT實現

2。3認證系統建立

接下來我們建立我們的認證服務。

單點登入SSO方法之SpringSecurity+JWT實現

匯入相關的依賴

org。springframework。boot spring-boot-starter-web org。springframework。boot spring-boot-starter-security security-jwt-common com。dpb 1。0-SNAPSHOT mysql mysql-connector-java 5。1。47 org。mybatis。spring。boot mybatis-spring-boot-starter 2。1。0 com。alibaba druid 1。1。10 org。springframework。boot spring-boot-configuration-processor true 1234567891011121314151617181920212223242526272829303132333435

建立配置檔案

spring: datasource: driver-class-name: com。mysql。jdbc。Driver url: jdbc:mysql://localhost:3306/srm username: root password: 123456 type: com。alibaba。druid。pool。DruidDataSourcemybatis: type-aliases-package: com。dpb。domain mapper-locations: classpath:mapper/*。xmllogging: level: com。dpb: debugrsa: key: pubKeyFile: c:\tools\auth_key\id_key_rsa。pub priKeyFile: c:\tools\auth_key\id_key_rsa1234567891011121314151617

單點登入SSO方法之SpringSecurity+JWT實現

提供公鑰私鑰的配置類

package com。dpb。config;import com。dpb。utils。RsaUtils;import lombok。Data;import org。springframework。boot。context。properties。ConfigurationProperties;import org。springframework。context。annotation。Configuration;import javax。annotation。PostConstruct;import java。security。PrivateKey;import java。security。PublicKey;/** * @program: springboot-54-security-jwt-demo * @description: * @author: 波波烤鴨 * @create: 2019-12-03 11:25 */@Data@ConfigurationProperties(prefix = “rsa。key”)public class RsaKeyProperties { private String pubKeyFile; private String priKeyFile; private PublicKey publicKey; private PrivateKey privateKey; /** * 系統啟動的時候觸發 * @throws Exception */ @PostConstruct public void createRsaKey() throws Exception { publicKey = RsaUtils。getPublicKey(pubKeyFile); privateKey = RsaUtils。getPrivateKey(priKeyFile); }}1234567891011121314151617181920212223242526272829303132333435363738

建立啟動類

/** * @program: springboot-54-security-jwt-demo * @description: 啟動類 * @author: 波波烤鴨 * @create: 2019-12-03 11:23 */@SpringBootApplication@MapperScan(“com。dpb。mapper”)@EnableConfigurationProperties(RsaKeyProperties。class)public class App { public static void main(String[] args) { SpringApplication。run(App。class,args); }}123456789101112131415

完成資料認證的邏輯

pojo

package com。dpb。domain;import com。fasterxml。jackson。annotation。JsonIgnore;import lombok。Data;import org。springframework。security。core。GrantedAuthority;/** * @program: springboot-54-security-jwt-demo * @description: * @author: 波波烤鴨 * @create: 2019-12-03 15:21 */@Datapublic class RolePojo implements GrantedAuthority { private Integer id; private String roleName; private String roleDesc; @JsonIgnore @Override public String getAuthority() { return roleName; }}12345678910111213141516171819202122232425

package com。dpb。domain;import com。fasterxml。jackson。annotation。JsonIgnore;import lombok。Data;import org。springframework。security。core。GrantedAuthority;import org。springframework。security。core。authority。SimpleGrantedAuthority;import org。springframework。security。core。userdetails。UserDetails;import java。util。ArrayList;import java。util。Collection;import java。util。List;/** * @program: springboot-54-security-jwt-demo * @description: * @author: 波波烤鴨 * @create: 2019-12-03 11:33 */@Datapublic class UserPojo implements UserDetails { private Integer id; private String username; private String password; private Integer status; private List roles; @JsonIgnore @Override public Collection<? extends GrantedAuthority> getAuthorities() { List auth = new ArrayList<>(); auth。add(new SimpleGrantedAuthority(“ADMIN”)); return auth; } @Override public String getPassword() { return this。password; } @Override public String getUsername() { return this。username; } @JsonIgnore @Override public boolean isAccountNonExpired() { return true; } @JsonIgnore @Override public boolean isAccountNonLocked() { return true; } @JsonIgnore @Override public boolean isCredentialsNonExpired() { return true; } @JsonIgnore @Override public boolean isEnabled() { return true; }}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869

Mapper介面

public interface UserMapper { public UserPojo queryByUserName(@Param(“userName”) String userName);}123

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”> 123456789

Service

public interface UserService extends UserDetailsService {}123

@Service@Transactionalpublic class UserServiceImpl implements UserService { @Autowired private UserMapper mapper; @Override public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException { UserPojo user = mapper。queryByUserName(s); return user; }}1234567891011121314

自定義認證過濾器

package com。dpb。filter;import com。dpb。config。RsaKeyProperties;import com。dpb。domain。RolePojo;import com。dpb。domain。UserPojo;import com。dpb。utils。JwtUtils;import com。fasterxml。jackson。databind。ObjectMapper;import net。bytebuddy。agent。builder。AgentBuilder;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。authority。SimpleGrantedAuthority;import org。springframework。security。core。userdetails。User;import org。springframework。security。core。userdetails。UserDetails;import org。springframework。security。web。authentication。UsernamePasswordAuthenticationFilter;import javax。servlet。FilterChain;import javax。servlet。ServletException;import javax。servlet。http。HttpServletRequest;import javax。servlet。http。HttpServletResponse;import java。io。IOException;import java。io。PrintWriter;import java。util。ArrayList;import java。util。HashMap;import java。util。List;import java。util。Map;/** * @program: springboot-54-security-jwt-demo * @description: * @author: 波波烤鴨 * @create: 2019-12-03 11:57 */public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter { private AuthenticationManager authenticationManager; private RsaKeyProperties prop; public TokenLoginFilter(AuthenticationManager authenticationManager, RsaKeyProperties prop) { this。authenticationManager = authenticationManager; this。prop = prop; } public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { try { UserPojo sysUser = new ObjectMapper()。readValue(request。getInputStream(), UserPojo。class); UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(sysUser。getUsername(), sysUser。getPassword()); return authenticationManager。authenticate(authRequest); }catch (Exception e){ try { response。setContentType(“application/json;charset=utf-8”); response。setStatus(HttpServletResponse。SC_UNAUTHORIZED); PrintWriter out = response。getWriter(); Map resultMap = new HashMap(); resultMap。put(“code”, HttpServletResponse。SC_UNAUTHORIZED); resultMap。put(“msg”, “使用者名稱或密碼錯誤!”); out。write(new ObjectMapper()。writeValueAsString(resultMap)); out。flush(); out。close(); }catch (Exception outEx){ outEx。printStackTrace(); } throw new RuntimeException(e); } } public void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { UserPojo user = new UserPojo(); user。setUsername(authResult。getName()); user。setRoles((List)authResult。getAuthorities()); String token = JwtUtils。generateTokenExpireInMinutes(user, prop。getPrivateKey(), 24 * 60); response。addHeader(“Authorization”, “Bearer ”+token); try { response。setContentType(“application/json;charset=utf-8”); response。setStatus(HttpServletResponse。SC_OK); PrintWriter out = response。getWriter(); Map resultMap = new HashMap(); resultMap。put(“code”, HttpServletResponse。SC_OK); resultMap。put(“msg”, “認證透過!”); out。write(new ObjectMapper()。writeValueAsString(resultMap)); out。flush(); out。close(); }catch (Exception outEx){ outEx。printStackTrace(); } }}1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889

自定義校驗token的過濾器

package com。dpb。filter;import com。dpb。config。RsaKeyProperties;import com。dpb。domain。Payload;import com。dpb。domain。UserPojo;import com。dpb。utils。JwtUtils;import com。fasterxml。jackson。databind。ObjectMapper;import org。springframework。security。authentication。AuthenticationManager;import org。springframework。security。authentication。UsernamePasswordAuthenticationToken;import org。springframework。security。core。context。SecurityContextHolder;import org。springframework。security。web。authentication。www。BasicAuthenticationFilter;import javax。servlet。FilterChain;import javax。servlet。ServletException;import javax。servlet。http。HttpServletRequest;import javax。servlet。http。HttpServletResponse;import java。io。IOException;import java。io。PrintWriter;import java。util。HashMap;import java。util。Map;/** * @program: springboot-54-security-jwt-demo * @description: * @author: 波波烤鴨 * @create: 2019-12-03 12:39 */public class TokenVerifyFilter extends BasicAuthenticationFilter { private RsaKeyProperties prop; public TokenVerifyFilter(AuthenticationManager authenticationManager, RsaKeyProperties prop) { super(authenticationManager); this。prop = prop; } public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { String header = request。getHeader(“Authorization”); if (header == null || !header。startsWith(“Bearer ”)) { //如果攜帶錯誤的token,則給使用者提示請登入! chain。doFilter(request, response); response。setContentType(“application/json;charset=utf-8”); response。setStatus(HttpServletResponse。SC_FORBIDDEN); PrintWriter out = response。getWriter(); Map resultMap = new HashMap(); resultMap。put(“code”, HttpServletResponse。SC_FORBIDDEN); resultMap。put(“msg”, “請登入!”); out。write(new ObjectMapper()。writeValueAsString(resultMap)); out。flush(); out。close(); } else { //如果攜帶了正確格式的token要先得到token String token = header。replace(“Bearer ”, “”); //驗證tken是否正確 Payload payload = JwtUtils。getInfoFromToken(token, prop。getPublicKey(), UserPojo。class); UserPojo user = payload。getUserInfo(); if(user!=null){ UsernamePasswordAuthenticationToken authResult = new UsernamePasswordAuthenticationToken(user。getUsername(), null, user。getAuthorities()); SecurityContextHolder。getContext()。setAuthentication(authResult); chain。doFilter(request, response); } } }}12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364

###編寫SpringSecurity的配置類

package com。dpb。config;import com。dpb。filter。TokenLoginFilter;import com。dpb。filter。TokenVerifyFilter;import com。dpb。service。UserService;import org。springframework。beans。factory。annotation。Autowired;import org。springframework。context。annotation。Bean;import org。springframework。context。annotation。Configuration;import org。springframework。security。config。annotation。authentication。builders。AuthenticationManagerBuilder;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;import org。springframework。security。config。http。SessionCreationPolicy;import org。springframework。security。crypto。bcrypt。BCryptPasswordEncoder;/** * @program: springboot-54-security-jwt-demo * @description: * @author: 波波烤鴨 * @create: 2019-12-03 12:41 */@Configuration@EnableWebSecurity@EnableGlobalMethodSecurity(securedEnabled=true)public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserService userService; @Autowired private RsaKeyProperties prop; @Bean public BCryptPasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } //指定認證物件的來源 public void configure(AuthenticationManagerBuilder auth) throws Exception { auth。userDetailsService(userService)。passwordEncoder(passwordEncoder()); } //SpringSecurity配置資訊 public void configure(HttpSecurity http) throws Exception { http。csrf() 。disable() 。authorizeRequests() 。antMatchers(“/user/query”)。hasAnyRole(“ADMIN”) 。anyRequest() 。authenticated() 。and() 。addFilter(new TokenLoginFilter(super。authenticationManager(), prop)) 。addFilter(new TokenVerifyFilter(super。authenticationManager(), prop)) 。sessionManagement()。sessionCreationPolicy(SessionCreationPolicy。STATELESS); }}1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556

啟動服務測試

啟動服務

單點登入SSO方法之SpringSecurity+JWT實現

透過Postman來訪問測試

單點登入SSO方法之SpringSecurity+JWT實現

單點登入SSO方法之SpringSecurity+JWT實現

根據token資訊我們訪問其他資源

單點登入SSO方法之SpringSecurity+JWT實現

2。4資源系統建立

說明

資源服務可以有很多個,這裡只拿產品服務為例,記住,資源服務中只能透過公鑰驗證認證。不能簽發token!建立產品服務並匯入jar包根據實際業務導包即可,咱們就暫時和認證服務一樣了。

接下來我們再建立一個資源服務

單點登入SSO方法之SpringSecurity+JWT實現

匯入相關的依賴

org。springframework。boot spring-boot-starter-web org。springframework。boot spring-boot-starter-security security-jwt-common com。dpb 1。0-SNAPSHOT mysql mysql-connector-java 5。1。47 org。mybatis。spring。boot mybatis-spring-boot-starter 2。1。0 com。alibaba druid 1。1。10 org。springframework。boot spring-boot-configuration-processor true 1234567891011121314151617181920212223242526272829303132333435

編寫產品服務配置檔案

切記這裡只能有公鑰地址!

server: port: 9002spring: datasource: driver-class-name: com。mysql。jdbc。Driver url: jdbc:mysql://localhost:3306/srm username: root password: 123456 type: com。alibaba。druid。pool。DruidDataSourcemybatis: type-aliases-package: com。dpb。domain mapper-locations: classpath:mapper/*。xmllogging: level: com。dpb: debugrsa: key: pubKeyFile: c:\tools\auth_key\id_key_rsa。pub123456789101112131415161718

編寫讀取公鑰的配置類

package com。dpb。config;import com。dpb。utils。RsaUtils;import lombok。Data;import org。springframework。boot。context。properties。ConfigurationProperties;import javax。annotation。PostConstruct;import java。security。PrivateKey;import java。security。PublicKey;/** * @program: springboot-54-security-jwt-demo * @description: * @author: 波波烤鴨 * @create: 2019-12-03 11:25 */@Data@ConfigurationProperties(prefix = “rsa。key”)public class RsaKeyProperties { private String pubKeyFile; private PublicKey publicKey; /** * 系統啟動的時候觸發 * @throws Exception */ @PostConstruct public void createRsaKey() throws Exception { publicKey = RsaUtils。getPublicKey(pubKeyFile); }}12345678910111213141516171819202122232425262728293031323334

編寫啟動類

package com。dpb;import com。dpb。config。RsaKeyProperties;import org。mybatis。spring。annotation。MapperScan;import org。springframework。boot。SpringApplication;import org。springframework。boot。autoconfigure。SpringBootApplication;import org。springframework。boot。context。properties。EnableConfigurationProperties;/** * @program: springboot-54-security-jwt-demo * @description: * @author: 波波烤鴨 * @create: 2019-12-03 17:23 */@SpringBootApplication@MapperScan(“com。dpb。mapper”)@EnableConfigurationProperties(RsaKeyProperties。class)public class App { public static void main(String[] args) { SpringApplication。run(App。class,args); }}1234567891011121314151617181920212223

複製認證服務中,使用者物件,角色物件和校驗認證的介面

複製認證服務中的相關內容即可

複製認證服務中SpringSecurity配置類做修改

package com。dpb。config;import com。dpb。filter。TokenVerifyFilter;import com。dpb。service。UserService;import org。springframework。beans。factory。annotation。Autowired;import org。springframework。context。annotation。Bean;import org。springframework。context。annotation。Configuration;import org。springframework。security。config。annotation。authentication。builders。AuthenticationManagerBuilder;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;import org。springframework。security。config。http。SessionCreationPolicy;import org。springframework。security。crypto。bcrypt。BCryptPasswordEncoder;/** * @program: springboot-54-security-jwt-demo * @description: * @author: 波波烤鴨 * @create: 2019-12-03 12:41 */@Configuration@EnableWebSecurity@EnableGlobalMethodSecurity(securedEnabled=true)public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserService userService; @Autowired private RsaKeyProperties prop; @Bean public BCryptPasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } //指定認證物件的來源 public void configure(AuthenticationManagerBuilder auth) throws Exception { auth。userDetailsService(userService)。passwordEncoder(passwordEncoder()); } //SpringSecurity配置資訊 public void configure(HttpSecurity http) throws Exception { http。csrf() 。disable() 。authorizeRequests() //。antMatchers(“/user/query”)。hasAnyRole(“USER”) 。anyRequest() 。authenticated() 。and() 。addFilter(new TokenVerifyFilter(super。authenticationManager(), prop)) // 禁用掉session 。sessionManagement()。sessionCreationPolicy(SessionCreationPolicy。STATELESS); }}1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556

去掉“增加自定義認證過濾器”即可!

編寫產品處理器

package com。dpb。controller;import org。springframework。security。access。annotation。Secured;import org。springframework。web。bind。annotation。RequestMapping;import org。springframework。web。bind。annotation。RestController;/** * @program: springboot-54-security-jwt-demo * @description: * @author: 波波烤鴨 * @create: 2019-12-03 11:55 */@RestController@RequestMapping(“/user”)public class UserController { @RequestMapping(“/query”) public String query(){ return “success”; } @RequestMapping(“/update”) public String update(){ return “update”; }}1234567891011121314151617181920212223242526

測試

單點登入SSO方法之SpringSecurity+JWT實現

搞定~