好好學習,天天向上
本文已收錄至我的Github倉庫DayDayUP:github.com/RobodLee/DayDayUP,歡迎Star,更多文章請前往:目錄導航
前言
Spring Security是一個功能強大且高度可定制的身份驗證和訪問控制框架,提供了完善的認證機制和方法級的授權功能,是一款非常優秀的權限管理框架,它的核心是一組過濾器鏈,不同的功能經由不同的過濾器,這篇文章就是想通過一個小案例將Spring Security整合到SpringBoot中去,要實作的功能就是在認證服務器上登錄,然后獲取Token,再訪問資源服務器中的資源,
基本概念
-
單點登錄
什么叫做單點登錄呢,就是在一個多應用系統中,只要在其中一個系統上登錄之后,不需要在其它系統上登錄也可以訪問其內容,舉個例子,京東那么復雜的系統肯定不會是單體結構,必然是微服務架構,比如訂單功能是一個系統,交易是一個系統......那么我在下訂單的時候登錄了,付錢難道還需要再登錄一次嗎,如果是這樣,用戶體驗也太差了吧,實作的流程就是我在下單的時候系統發現我沒登錄就讓我登錄,登錄完了之后系統回傳給我一個Token,就類似于身份證的東西;然后我想去付錢的時候就把Token再傳到交易系統中,然后交易系統驗證一下Token就知道是誰了,就不需要再讓我登錄一次,
-
JWT
上面提到的Token就是JWT(JSON Web Token),是一種用于通信雙方之間傳遞安全資訊的簡潔的、URL安全的表述性宣告規范,一個JWT實際上就是一個字串,它由三部分組成,頭部、載荷與簽名,為了能夠直觀的看到JWT的結構,我畫了一張思維導圖:
最終生成的JWT令牌就是下面這樣,有三部分,用 . 分隔,
base64UrlEncode(JWT 頭)+"."+base64UrlEncode(載荷)+"."+HMACSHA256(base64UrlEncode(JWT 頭) + "." + base64UrlEncode(有效載荷),密鑰)
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
-
RSA
從上面的例子中可以看出,JWT在加密解密的時候都用到了同一個密鑰 “ robod666 ”,這將會帶來一個弊端,如果被黑客知道了密鑰的內容,那么他就可以去偽造Token了,所以為了安全,我們可以使用非對稱加密演算法RSA,
RSA的基本原理有兩點:
- 私鑰加密,持有私鑰或公鑰才可以解密
- 公鑰加密,持有私鑰才可解密
認證服務器用戶登錄功能
前期準備
介紹完了基本概念之后就可以開始整合了,受限于篇幅,只貼最核心的代碼,其它內容請小伙伴們去原始碼中找,地址在文末, 首先需要準備好資料庫:
-- ----------------------------
-- Table structure for sys_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role` (
`ID` int(11) NOT NULL AUTO_INCREMENT COMMENT '編號',
`ROLE_NAME` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '角色名稱',
`ROLE_DESC` varchar(60) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '角色描述',
PRIMARY KEY (`ID`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
-- ----------------------------
-- Records of sys_role
-- ----------------------------
INSERT INTO `sys_role` VALUES (1, 'ROLE_USER', '基本角色');
INSERT INTO `sys_role` VALUES (2, 'ROLE_ADMIN', '超級管理員');
INSERT INTO `sys_role` VALUES (3, 'ROLE_PRODUCT', '管理產品');
INSERT INTO `sys_role` VALUES (4, 'ROLE_ORDER', '管理訂單');
-- ----------------------------
-- Table structure for sys_user
-- ----------------------------
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '用戶名稱',
`password` varchar(120) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '密碼',
`status` int(1) NULL DEFAULT 1 COMMENT '1開啟0關閉',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
-- ----------------------------
-- Records of sys_user
-- ----------------------------
INSERT INTO `sys_user` VALUES (1, 'xiaoming', '$2a$10$CYX9OMv0yO8wR8rE19N2fOaXDJondci5uR68k2eQJm50q8ESsDMlC', 1);
INSERT INTO `sys_user` VALUES (2, 'xiaoma', '$2a$10$CYX9OMv0yO8wR8rE19N2fOaXDJondci5uR68k2eQJm50q8ESsDMlC', 1);
-- ----------------------------
-- Table structure for sys_user_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_user_role`;
CREATE TABLE `sys_user_role` (
`UID` int(11) NOT NULL COMMENT '用戶編號',
`RID` int(11) NOT NULL COMMENT '角色編號',
PRIMARY KEY (`UID`, `RID`) USING BTREE,
INDEX `FK_Reference_10`(`RID`) USING BTREE,
CONSTRAINT `FK_Reference_10` FOREIGN KEY (`RID`) REFERENCES `sys_role` (`ID`) ON DELETE RESTRICT ON UPDATE RESTRICT,
CONSTRAINT `FK_Reference_9` FOREIGN KEY (`UID`) REFERENCES `sys_user` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
-- ----------------------------
-- Records of sys_user_role
-- ----------------------------
INSERT INTO `sys_user_role` VALUES (1, 1);
INSERT INTO `sys_user_role` VALUES (2, 1);
INSERT INTO `sys_user_role` VALUES (1, 3);
INSERT INTO `sys_user_role` VALUES (2, 4);
SET FOREIGN_KEY_CHECKS = 1;
一共三張表,分別是用戶表,角色表,用戶-角色表,用戶是登錄用的,密碼其實就是加密過的字串,內容是“ 123 ”;角色是做權限控制時用的,
然后創建一個空的父工程SpringSecurityDemo,然后在父工程里面創建一個Module作為認證服務,名叫authentication_server,添加必要的依賴,(內容較占篇幅,有需要的去原始碼中獲取,原始碼地址見文末),
專案的組態檔內容截取了核心的部分貼在下面:
…………
# 配置了公鑰和私鑰的位置
rsa:
key:
pubKeyPath: C:\Users\robod\Desktop\auth_key\id_key_rsa.pub
priKeyPath: C:\Users\robod\Desktop\auth_key\id_key_rsa
最后的公私鑰的標簽是自定義的,并不是Spring提供的標簽,后面我們會在RSA的配置類中去加載這一部分內容,
為了方便起見,我們還可以準備幾個工具類(內容較占篇幅,有需要的去原始碼中獲取,原始碼地址見文末):
- JsonUtils:提供了json相關的一些操作;
- JwtUtils:生成token以及校驗token相關方法;
- RsaUtils:生成公鑰私鑰檔案,以及從檔案中讀取公鑰私鑰,
我們可以將載荷單獨封裝成一個物件:
@Data
public class Payload<T> {
private String id;
private T userInfo;
private Date expiration;
}
現在再去寫一個測驗類,呼叫RsaUtils中的相應方法去生成公鑰和私鑰,那公鑰私鑰生成好了在使用的時候是怎么獲取的呢?為了解決這個問題,我們需要創建一個RSA的配置類,
@Data
@ConfigurationProperties("rsa.key") //指定組態檔的key
public class RsaKeyProperties {
private String pubKeyPath;
private String priKeyPath;
private PublicKey publicKey;
private PrivateKey privateKey;
@PostConstruct
public void createKey() throws Exception {
this.publicKey = RsaUtils.getPublicKey(pubKeyPath);
this.privateKey = RsaUtils.getPrivateKey(priKeyPath);
}
}
首先我們使用了@ConfigurationProperties注解去指定公鑰私鑰路徑的key,然后在構造方法中就可以去獲取到公鑰私鑰的內容了,這樣在需要公鑰私鑰的時候就可以直接呼叫這個類了,但是不放入Spring容器中怎么呼叫這個類,所以在啟動類中添加一個注解:
@EnableConfigurationProperties(RsaKeyProperties.class)
這表示把RSA的配置類放入Spring容器中,
用戶登錄
在實作用戶登錄的功能之前,先說一下登錄的相關內容,關于登錄流程我在網上看了篇文章感覺挺好的,貼出來給小伙伴們看看:
https://www.jianshu.com/p/a65f883de0c1
首先會進入UsernamePasswordAuthenticationFilter并且設定權限為null和是否授權為false,然后進入ProviderManager查找支持UsernamepasswordAuthenticationToken的provider并且呼叫provider.authenticate(authentication);再然后就是
UserDetailsService介面的實作類(也就是自己真正具體的業務了),這時候都檢查過了后,就會回呼UsernamePasswordAuthenticationFilter并且設定權限(具體業務所查出的權限)和設定授權為true(因為這時候確實所有關卡都檢查過了),
在上面這段話中,提到了一個UsernamePasswordAuthenticationFilter,我們一開始進入的就是這個過濾器的attemptAuthentication()方法,但是這個方法是從form表單中獲取用戶名密碼,和我們的需求不符,所以我們需要重寫這個方法,然后經過一系列的周轉,進入到了UserDetailsService.loadUserByUsername()方法中,所以我們為了實作自己的業務邏輯,需要去實作這個方法,這個方法回傳的是一個UserDetails介面物件,如果想回傳自定義的物件,可以去實作這個介面,最終用戶驗證成功之后,呼叫的是UsernamePasswordAuthenticationFilter的父類AbstractAuthenticationProcessingFilter.successfulAuthentication()方法,我們也需要去重寫這個方法去實作我們自己的需求,
所以現在就來實作一下上面說的這些東西吧??
@Data
public class SysUser implements UserDetails {
private Integer id;
private String username;
private String password;
private Integer status;
private List<SysRole> roles = new ArrayList<>(); //SysRole封裝了角色資訊,和登錄無關,我放在后面講
//這里還有幾個UserDetails中的方法,我就不貼代碼了
}
我們自定義了一個SysUser類去實作UserDetails介面,然后添加了幾個自定義的欄位?
public interface UserService extends UserDetailsService {
}
//-----------------------------------------------------------
@Service("userService")
public class UserServiceImpl implements UserService {
…………
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser sysUser = userMapper.findByUsername(username);
return sysUser;
}
}
在?這段代碼中,我們先定義了一個介面UserService去繼承UserDetailsService,然后用UserServiceImpl實作了UserService,就相當于UserServiceImpl實作了UserDetailsService,這樣我們就可以去實作loadUserByUsername()方法,內容很簡單,就是用用戶名去資料庫中查出對應的SysUser,然后具體的驗證流程就可以交給其它的過濾器去實作了,我們就不用管了,
前面提到了需要去重寫attemptAuthentication()和successfulAuthentication()方法,那就自定義一個過濾器去繼承UsernamePasswordAuthenticationFilter然后重寫這兩個方法吧??
public class JwtLoginFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;
private RsaKeyProperties rsaKeyProperties;
public JwtLoginFilter(AuthenticationManager authenticationManager, RsaKeyProperties rsaKeyProperties) {
this.authenticationManager = authenticationManager;
this.rsaKeyProperties = rsaKeyProperties;
}
//這個方法是用來去嘗試驗證用戶的
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
try {
SysUser user = JSONObject.parseObject(request.getInputStream(),SysUser.class);
return authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
user.getUsername(),
user.getPassword())
);
} catch (Exception e) {
try {
response.setContentType("application/json;charset=utf-8");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
PrintWriter out = response.getWriter();
Map<String, Object> map = new HashMap<>();
map.put("code", HttpServletResponse.SC_UNAUTHORIZED);
map.put("message", "賬號或密碼錯誤!");
out.write(new ObjectMapper().writeValueAsString(map));
out.flush();
out.close();
} catch (Exception e1) {
e1.printStackTrace();
}
throw new RuntimeException(e);
}
}
//成功之后執行的方法
@Override
public void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
SysUser sysUser = new SysUser();
sysUser.setUsername(authResult.getName());
sysUser.setRoles((List<SysRole>) authResult.getAuthorities());
String token = JwtUtils.generateTokenExpireInMinutes(sysUser,rsaKeyProperties.getPrivateKey(),24*60);
response.addHeader("Authorization", "RobodToken " + token); //將Token資訊回傳給用戶
try {
//登錄成功時,回傳json格式進行提示
response.setContentType("application/json;charset=utf-8");
response.setStatus(HttpServletResponse.SC_OK);
PrintWriter out = response.getWriter();
Map<String, Object> map = new HashMap<String, Object>(4);
map.put("code", HttpServletResponse.SC_OK);
map.put("message", "登陸成功!");
out.write(new ObjectMapper().writeValueAsString(map));
out.flush();
out.close();
} catch (Exception e1) {
e1.printStackTrace();
}
}
}
代碼的邏輯還是很清晰的,我就不去講解了,
現在重點來了,Spring Security怎么知道我們要去呼叫自己的UserService和自定義的過濾器呢?所以我們需要配置一下,這也是使用Spring Security的一個核心——>配置類??
@Configuration
@EnableWebSecurity //這個注解的意思是這個類是Spring Security的配置類
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
…………
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
//認證用戶的來源
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService).passwordEncoder(passwordEncoder());
}
//配置SpringSecurity相關資訊
@Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable() //關閉csrf
.addFilter(new JwtLoginFilter(super.authenticationManager(),rsaKeyProperties))
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); //禁用session
}
}
在配置類中,配置了認證用戶的來源和添加了自定義的過濾器,這樣就可以實作登錄的功能了,
可以看到,現在已經成功登錄了,但是這個/login是從哪兒來的呢,這個是Spring Security自己提供的,用戶名的鍵必須是”username“,密碼的鍵必須是 ”password“,提交方式必須是POST,
總結一下,實作登錄的功能需要做哪些操作:
- 認證用戶實作UserDetails介面
- 用戶來源的Service實作UserDetailsService介面,實作loadUserByUsername()方法,從資料庫中獲取資料
- 實作自己的過濾器繼承UsernamePasswordAuthenticationFilter,重寫attemptAuthentication()和successfulAuthentication()方法實作自己的邏輯
- Spring Security的配置類繼承自WebSecurityConfigurerAdapter,重寫里面的兩個config()方法
- 如果使用RSA非對稱加密,就準備好RSA的配置類,然后在啟動類中加入注解將其加入IOC容器中
資源服務器權限校驗
在這一小節,我們要實作去訪問資源服務器中的資源,并進行鑒權的操作,在父工程SpringSecirityDemo中再創建一個模塊recourse_server,因為我們現在并不需要從資料庫中獲取用戶資訊,所以就不需要自己去定義Service和Mapper了,也不需要登錄的過濾器了,下面這張目錄結構圖是資源服務工程所需要的所有東西,
SysRole上一節中用到了但是沒有詳細說明,這個類是用來封裝角色資訊的,做鑒權的時候用的,實作了GrantedAuthority介面:
@Data
public class SysRole implements GrantedAuthority {
private Integer id;
private String roleName;
private String roleDesc;
/**
* 如果授予的權限可以當作一個String的話,就可以回傳一個String
* @return
*/
@JsonIgnore
@Override
public String getAuthority() {
return roleName;
}
}
里面實作了getAuthority方法,直接回傳roleName即可,roleName是角色名,
客戶端將Token傳到資源服務器中,服務器需要對Token進行校驗并取出其中的載荷資訊,所以我們可以自定義一個過濾器繼承自BasicAuthenticationFilter,然后重寫doFilterInternal()方法,實作自己的邏輯,
public class JwtVerifyFilter extends BasicAuthenticationFilter {
…………
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
String header = request.getHeader("Authorization");
//沒有登錄
if (header == null || !header.startsWith("RobodToken ")) {
chain.doFilter(request, response);
response.setContentType("application/json;charset=utf-8");
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
PrintWriter out = response.getWriter();
Map<String, Object> map = new HashMap<String, Object>(4);
map.put("code", HttpServletResponse.SC_FORBIDDEN);
map.put("message", "請登錄!");
out.write(new ObjectMapper().writeValueAsString(map));
out.flush();
out.close();
return;
}
//登錄之后從token中獲取用戶資訊
String token = header.replace("RobodToken ","");
SysUser sysUser = JwtUtils.getInfoFromToken(token, rsaKeyProperties.getPublicKey(), SysUser.class).getUserInfo();
if (sysUser != null) {
Authentication authResult = new UsernamePasswordAuthenticationToken
(sysUser.getUsername(),null,sysUser.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authResult);
chain.doFilter(request, response);
}
}
}
在這段代碼中,先是從請求頭中獲取"Authorization"的值,如果值未null或者不是以我們規定的 “RobodToken ” 開頭就說明不是我們設定的Token,就是沒登錄,提示用戶登錄,有Token的話就呼叫JwtUtils.getInfoFromToken()去驗證并獲取載荷的內容,驗證通過的話就在Authentication的構造方法中把角色資訊傳進去,然后交給其它過濾器去執行即可,
私鑰應該只保存在認證服務器中,所以資源服務器中只要存公鑰就可以了,
…………
rsa:
key:
pubKeyPath: C:\Users\robod\Desktop\auth_key\id_key_rsa.pub
@Data
@ConfigurationProperties("rsa.key") //指定組態檔的key
public class RsaKeyProperties {
private String pubKeyPath;
private PublicKey publicKey;
@PostConstruct
public void createKey() throws Exception {
this.publicKey = RsaUtils.getPublicKey(pubKeyPath);
}
}
接下來就是Spring Security核心的組態檔了??
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true) //開啟權限控制的注解支持,securedEnabled表示SpringSecurity內部的權限控制注解開關
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
…………
//配置SpringSecurity相關資訊
@Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable() //關閉csrf
.authorizeRequests()
.antMatchers("/**").hasAnyRole("USER") //角色資訊
.anyRequest() //其它資源
.authenticated() //表示其它資源認證通過后
.and()
.addFilter(new JwtVerifyFilter(super.authenticationManager(),rsaKeyProperties))
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); //禁用session
}
}
這里面有個注解 @EnableGlobalMethodSecurity(securedEnabled = true),這個注解的意思是開啟權限控制的注解支持,然后添加了自定義的Token決議過濾器,最后在需要進行權限控制的方法上添加注解即可??
@RestController
@RequestMapping("/product")
public class ProductController {
@Secured("ROLE_PRODUCT")
@RequestMapping("/findAll")
public String findAll() {
return "產品串列查詢成功";
}
}
好了,這樣findAll方法就需要有"ROLE_PRODUCT"權限才能訪問,我們來測驗一下:
登錄成功之后,回應頭中有服務器回傳的Token資訊,把它復制下來,然后添加到我們請求的請求頭中,
可以看到,現在已經成功訪問到資源了,再來換個沒有權限的用戶登錄測驗一下:
請求被拒絕了,說明權限控制功能是沒有問題的,總結一下步驟:
- 封裝權限資訊的類實作GrantedAuthority介面,并實作里面的getAuthority()方法
- 實作自己的Token校驗過濾器繼承自BasicAuthenticationFilter,并重寫doFilterInternal()方法,實作自己的業務邏輯
- 撰寫Spring Security的配置類繼承WebSecurityConfigurerAdapter,重寫configure()方法添加自定義的過濾器,并添加@EnableGlobalMethodSecurity(securedEnabled = true)注解開啟注解權限控制的功能
- 如果使用RSA非對稱加密,就準備好RSA的配置類,然后在啟動類中加入注解將其加入IOC容器中,注意這里不要只要配置公鑰即可
總結
SpringBoot 整合 Spring Security到這里就結束了,文章只是簡單的說了一下整合的流程,很多其它的東西都沒有說,比如各個過濾器都有什么作用等,還有,這里采用的認證服務器和資源服務器分離的方式,要是集成在一起也是可以的,類似的問題還有很多,小伙伴們就自行研究吧,問了讓文章不會太臃腫,很多代碼都沒有貼出來,有需要的小伙伴點擊下面的鏈接就可以下載了,
點擊下載原始碼
如果我的文章對你有些幫助,不要忘了點贊,收藏,轉發,關注,要是有什么好的意見歡迎在下方留言,讓我們下期再見!
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/84950.html
標籤:Java
上一篇:Runtime類的簡單應用
