javascript
Spring Security入门01-22 登录验证功能
課程鏈接:SpringSecurity框架教程
開始時間:2022-07-17
快速入門
搭建一個Spring Boot項目
添加基礎依賴和創建啟動類和controller
controller
啟動類
package com.bupt.security_test;import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplication public class SecurityTestApplication {public static void main(String[] args) {SpringApplication.run(SecurityTestApplication.class, args);}}此時訪問頁面localhost:8080/hello
可以看到顯示hello這一個單詞
引入Security的依賴
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency>此時我們再去訪問
localhost:8080/hello
會自動跳轉到security帶的登錄界面
登錄名默認是user
密碼會在控制臺打印給你
如果輸入錯誤的賬戶密碼
輸入正確
默認有一個退出的接口logout
認證
登錄校驗流程圖
Spring Security流程
本質是一個過濾器鏈
-
UsernamePasswordAuthenticationFilter:負責處理我們在登陸頁面填寫了用戶名密碼后的登陸請求。入門案例的認證工作主要有它負責。
-
ExceptionTranslationFilter:處理過濾器鏈中拋出的任何AccessDeniedException和AuthenticationException 。 發現異常并重定向
-
FilterSecurityInterceptor:負責權限校驗的過濾器。(他實現了過濾器接口)
沒想到還有重溫之前學攔截器的部分,參考博客
這里只是選了三個典型的,還有其他過濾器,暫不研究
我們可以查看有哪些過濾器
認證流程詳解
區分一下概念
-
Authentication接口: 它的實現類,表示當前訪問系統的用戶,封裝了用戶相關信息。
-
AuthenticationManager接口:定義了認證Authentication的方法
-
UserDetailsService接口:加載用戶特定數據的核心接口。里面定義了一個根據用戶名查詢用戶信息的方法。
-
UserDetails接口:提供核心用戶信息。通過UserDetailsService根據用戶名獲取處理的用戶信息要封裝成UserDetails對象返回。然后將這些信息封裝到Authentication對象中。
-
第一步:提交用戶名和密碼 傳入到UsernamePasswordAuthenticationFilter,這個過濾器把用戶名和密碼封裝為一個Authentication對象,此時還沒有權限信息
-
第二步 調用authenticate方法進行認證,鏈條走到AuthenticationManager,但他也沒完,繼續調用DaoAuthenticationProvider的authenticate方法進行認證
-
第三步 即使已經經過了三個過濾器,還是沒有認證,需要繼續調用loadUserByUsername方法查詢用戶,走到第四個鏈條處了
-
第四步 根據用戶名去查詢用戶及該用戶對應的權限信息,InMemoryUserDetailsManager是在內存中查找的,并把對應用戶信息添加上權限信息封裝成UserDetails對象進行返回
-
第五步 UserDetails返回到Provider處,判斷PasswordEncoder對比UserDetails中的密碼和Authentication的密碼是否正確,如果正確就把UserDetails中的權限信息設置到Authentication對象中
-
第六步 返回Authentication對象到第一個過濾器處,如果成功返回,就使用SecurityContextHolder.getContext().setAuthentication方法村吃該對象,其他過濾器可以通過SecurityContextHolder來獲取當前用戶信息
我們想想,在第四步中,查詢的信息是在內存中查的,而我們需要從數據庫查,那就得自己來實現這個接口
之后再請求
那么我們經過JWT拿到UserID后,怎么獲取完整信息呢?再去數據庫一條條查?會增大IO開銷
因此,Redis閃亮登場。
那JWT要去Redis里面查,那總得Redis里面有東西吧,什么時候放呢?就在我們登錄接口那里,如果認證通過,生成一個JWT的同時,再把UserID:用戶信息存入Redis里面即可
解決問題
思路分析
-
登錄
①自定義登錄接口
調用ProviderManager的方法進行認證 如果認證通過生成jwt
把用戶信息存入redis中
②自定義UserDetailsService
在這個實現類中去查詢數據庫 -
校驗:
①定義Jwt認證過濾器
獲取token
解析token獲取其中的userid
從redis中獲取用戶信息
存入SecurityContextHolder
準備工作,添加所需要的Maven依賴以及相應的工具類
功能實現
數據庫校驗用戶
我們可以自定義一個UserDetailsService,讓SpringSecurity使用我們的UserDetailsService。我們自己的UserDetailsService可以從數據庫中查詢用戶名和密碼。
建表并引入MybatisPuls和mysql驅動的依賴
配置數據庫信息
配置mapper
在主啟動類上掃描mapper
@MapperScan("com.bupt.security_test.mapper") public class SecurityTestApplication測試一下
@SpringBootTest public class SecurityTestApplicationTests {@Autowiredprivate UserMapper userMapper;@Testpublic void testUserMapper() {List<User> users = userMapper.selectList(null);System.out.println(users);} }讀取到了數據庫數據,說明MyBatisPlus沒問題
那我們就要嘗試鏈接自己的數據庫的賬號密碼了
創建一個類實現UserDetailsService接口,重寫其中的方法。
這里的==@service注解不能忘記==
返回的實體類LoginUser我們也定義一下,因為我們的User本身和UserService沒關系,需要借助LoginUser包裝一下
注意后面幾個方法雖然沒實現,但返回改為了true而不是默認的false
測試要求輸入賬號密碼
現階段要把password密碼明文前面加上{noop}
不然識別不出來
這個括號里面是說明該密碼采取什么方式進行的加密,{noop}表示沒有加密
為什么呢,我們再看看上面的圖
實際項目中我們不會把密碼明文存儲在數據庫中。
? 默認使用的PasswordEncoder要求數據庫中的密碼格式為:{id}password 。它會根據id去判斷密碼的加密方式。但是我們一般不會采用這種方式。所以就需要替換PasswordEncoder。
我們一般使用SpringSecurity為我們提供的BCryptPasswordEncoder。
我們只需要使用把BCryptPasswordEncoder對象注入Spring容器中,SpringSecurity就會使用該PasswordEncoder來進行密碼校驗。
我們可以定義一個SpringSecurity的配置類,SpringSecurity要求這個配置類要繼承WebSecurityConfigurerAdapter。
配置一下SecurityConfig
package com.bupt.security_test.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter {@Beanpublic PasswordEncoder passwordEncoder(){return new BCryptPasswordEncoder();} }寫一個測試類看看
@Testpublic void TestBCryptPasswordEncoder() {BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();String encode1 = bCryptPasswordEncoder.encode("1234");String encode2 = bCryptPasswordEncoder.encode("1234");System.out.println(encode1);System.out.println(encode2);}我們打印輸出,會發現每次encode的結果都不一致
$2a$10$NMAeABItflg2UyrWNuOwvu0hFhEqGIk.zyx0RUJbeAm6jrjQq54oi $2a$10$p5J1O.5THmLqv9ATSv/juep3QxhsUOyGTh/5bL51.8kcwKMNNYlle我們存數據庫的密碼不是明文,而是encode后的結果
但不管怎樣,用誰加密,對應匹配他還是認識
再看一看
輸出
encode1輸出$2a$10$PJNq2HpUoHdj52zp3dPdNO9wKTpuGJHYeF.nX.NyDjt8jWUBqRM46 encode1判斷是否能匹配rawPasswordtrue encode2輸出$2a$10$WAwFkHZ8xWoR3e5D9zkWgefxIrrRsZv8B093x9FYA7I/jCqMw8mvi encode2判斷是否能匹配rawPasswordtrue這里為什么不同的編碼都能匹配上呢,知識超綱了,這好像是什么 鹽 值
配置好之后,我們直接輸入用戶密碼就不行了,要更新數據庫的密碼為加密后的密碼才行
JWT工具類
package com.bupt.security_test.utils;import io.jsonwebtoken.Claims; import io.jsonwebtoken.JwtBuilder; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm;import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import java.util.Base64; import java.util.Date; import java.util.UUID;/*** JWT工具類*/ public class JwtUtil {//有效期為public static final Long JWT_TTL = 60 * 60 * 1000L;// 60 * 60 *1000 一個小時//設置秘鑰明文public static final String JWT_KEY = "bupt";public static String getUUID() {String token = UUID.randomUUID().toString().replaceAll("-", "");return token;}/*** 生成jtw** @param subject token中要存放的數據(json格式)* @return*/public static String createJWT(String subject) {JwtBuilder builder = getJwtBuilder(subject, null, getUUID());// 設置過期時間return builder.compact();}/*** 生成jwt** @param subject token中要存放的數據(json格式)* @param ttlMillis token超時時間* @return*/public static String createJWT(String subject, Long ttlMillis) {JwtBuilder builder = getJwtBuilder(subject, ttlMillis, getUUID());// 設置過期時間return builder.compact();}private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) {SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;SecretKey secretKey = generalKey();long nowMillis = System.currentTimeMillis();Date now = new Date(nowMillis);if (ttlMillis == null) {ttlMillis = JwtUtil.JWT_TTL;}long expMillis = nowMillis + ttlMillis;Date expDate = new Date(expMillis);return Jwts.builder().setId(uuid) //唯一的ID.setSubject(subject) // 主題 可以是JSON數據.setIssuer("jdh") // 簽發者.setIssuedAt(now) // 簽發時間.signWith(signatureAlgorithm, secretKey) //使用HS256對稱加密算法簽名, 第二個參數為秘鑰.setExpiration(expDate);}/*** 創建token** @param id* @param subject* @param ttlMillis* @return*/public static String createJWT(String id, String subject, Long ttlMillis) {JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id);// 設置過期時間return builder.compact();}public static void main(String[] args) throws Exception {String jwt = createJWT("2123");System.out.println(jwt);//base64解碼Claims claims = parseJWT(jwt);System.out.println(claims.getSubject());//String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJjYWM2ZDVhZi1mNjVlLTQ0MDAtYjcxMi0zYWEwOGIyOTIwYjQiLCJzdWIiOiJzZyIsImlzcyI6InNnIiwiaWF0IjoxNjM4MTA2NzEyLCJleHAiOjE2MzgxMTAzMTJ9.JVsSbkP94wuczb4QryQbAke3ysBDIL5ou8fWsbt_ebg";//Claims claims = parseJWT(token);//System.out.println(claims);}/*** 生成加密后的秘鑰 secretKey** @return*/public static SecretKey generalKey() {byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");return key;}/*** 解析** @param jwt* @return* @throws Exception*/public static Claims parseJWT(String jwt) throws Exception {SecretKey secretKey = generalKey();return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwt).getBody();}}執行主方法得到
eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJiMGQxYmNjODI0NzI0ODljYjZlMDdiMGIxNDkyMzhkZiIsInN1YiI6IjIxMjMiLCJpc3MiOiJqZGgiLCJpYXQiOjE2NTgwNDM0ODQsImV4cCI6MTY1ODA0NzA4NH0.R4WV-QID5r2gACp7qXYV_fXy13VFAV0cjBzUfgFO-v8 2123登錄接口
自定義登錄接口
調用ProviderManager的方法進行認證 如果認證通過生成jwt
自定義登陸接口,然后讓SpringSecurity對這個接口放行,讓用戶訪問這個接口的時候不用登錄也能訪問。
在接口中我們通過AuthenticationManager的authenticate方法來進行用戶認證,所以需要在SecurityConfig中配置把AuthenticationManager注入容器。
認證成功的話要生成一個jwt,放入響應中返回。并且為了讓用戶下回請求時能通過jwt識別出具體的是哪個用戶,我們需要把用戶信息存入redis,可以把用戶id作為key。
首先說如何放行
SecurityConfig重寫方法
然后正常的controller service serviceimpl走一遍
package com.bupt.security_test.controller;import com.bupt.security_test.domain.ResponseResult; import com.bupt.security_test.domain.User; import com.bupt.security_test.service.LoginService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController;@RestController public class LoginController {@Autowiredprivate LoginService loginService;@PostMapping("/user/login")//RequestBody用來拿JSON傳過來的用戶名和密碼public ResponseResult login(@RequestBody User user) {//登錄return loginService.login(user);} } public interface LoginService {ResponseResult login(User user); } @Service public class LoginServiceImpl implements LoginService {//這個類在SecurityConfig里面實現的@Autowiredprivate AuthenticationManager authenticationManager;@Autowiredprivate RedisCache redisCache;@Overridepublic ResponseResult login(User user) {//AuthenticationManager authenticate進行用戶認證//用戶名和密碼先封裝,而Authentication是接口,我們需要找一個他的實現類//在接口名上 ctrl+alt+鼠標左鍵,可以看其常用實現類UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword());//通過authenticationManager來實現認證,會調用UserDetailsServiceImplAuthentication authenticate = authenticationManager.authenticate(authenticationToken);//如果認證沒通過,給出對應的提示if (Objects.isNull(authenticate)) {throw new RuntimeException("登錄失敗");}//如果認證通過了,使用userid生成一個jwt jwt存入ResponseResult返回LoginUser loginUser = (LoginUser) authenticate.getPrincipal();String userid = loginUser.getUser().getId().toString();String jwt = JwtUtil.createJWT(userid);Map<String, String> map = new HashMap<>();map.put("token", jwt);//把完整的用戶信息存入redis userid作為keyredisCache.setCacheObject("login:" + userid, loginUser);return new ResponseResult(200, "登錄成功", map);} }而我們需要先暴露AuthenticationManager
因此要在SecurityConfig添加
使用postman進行debug
debug看,
//通過authenticationManager來實現認證,會調用UserDetailsServiceImplAuthentication authenticate = authenticationManager.authenticate(authenticationToken);拿到的 authenticate包含的principal下有user信息
集成redis后,我們再來看看測試結果
我們把這個token拿回工具類解析
得到的輸出為1,即該用戶的id為1
認證過濾器
校驗:
①定義Jwt認證過濾器
獲取token
解析token獲取其中的userid
從redis中獲取用戶信息
存入SecurityContextHolder
過濾器代碼
package com.bupt.security_test.filter;import com.bupt.security_test.domain.LoginUser; import com.bupt.security_test.utils.JwtUtil; import com.bupt.security_test.utils.RedisCache; import io.jsonwebtoken.Claims; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import org.springframework.web.filter.OncePerRequestFilter;import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.Objects;//認證過濾器,那這個過濾器需要在SecurityConfig里面配置他出現的位置 //他要出現在UsernamePasswordAuthenticationFilter之前 @Component public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {@Autowiredprivate RedisCache redisCache;@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {//獲取token,從請求頭里面拿String token = request.getHeader("token");//沒有token,直接放行//放行后會去執行后面的過濾器//走完后面的過濾器,響應回來的時候還會走一遍過濾器鏈//如果沒有return,回來的時候還會執行一次,就會報錯,因為根本沒有token//登錄接口也會從這里過,但因為沒有token就被放行了if (!StringUtils.hasText(token)) {filterChain.doFilter(request, response);return;}String userid;//解析tokentry {Claims claims = JwtUtil.parseJWT(token);userid = claims.getSubject();} catch (Exception e) {e.printStackTrace();throw new RuntimeException("token非法");}//從redis里面獲取用戶信息String redisKey = "login:" + userid;LoginUser loginUser = redisCache.getCacheObject(redisKey);//存入SecurityContextHolderif (Objects.isNull(loginUser)) {throw new RuntimeException("用戶未登錄");}//存入SecurityContextHolder//TODO 獲取權限信息封裝到Authentication中UsernamePasswordAuthenticationToken authenticationToken =new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());SecurityContextHolder.getContext().setAuthentication(authenticationToken);//放行filterChain.doFilter(request, response);} }配置過濾器位置
SecurityConfig
測試一下,發送post請求
debug發現在
直接就放行了
然后一直走,完成登錄后就返回token
此時我們再來測試一下hello接口
被攔截下來了,需要補充token信息
這樣就能訪問了
退出登錄
清空redis SecurityContextHolder
//退出登錄@RequestMapping("/user/logout")public ResponseResult logout() {return loginService.logout();}實現類
//退出登錄@Overridepublic ResponseResult logout() {//獲取SecurityContextHolder中的用戶idUsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();LoginUser loginUser = (LoginUser) authentication.getPrincipal();Long userid = loginUser.getUser().getId();//用戶如果是未登錄狀態發起退出請求會被攔截下來,根本到不了這個方法//刪除redis中的值redisCache.deleteObject("login:" + userid);return new ResponseResult(200, "注銷成功");}
此時我們再使用原來的token訪問就不行
結束時間:2022-07-17
總結
以上是生活随笔為你收集整理的Spring Security入门01-22 登录验证功能的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: iphone的致命硬伤
- 下一篇: JavaScript的三级联动