SpringSecurity 自定義認(rèn)證登錄的項(xiàng)目實(shí)踐
前言
現(xiàn)在登錄方式越來(lái)越多,傳統(tǒng)的賬號(hào)密碼登錄已經(jīng)不能滿足我們的需求??赡芪覀冞€需要手機(jī)驗(yàn)證碼登錄,郵箱驗(yàn)證碼登錄,一鍵登錄等。這時(shí)候就需要我們自定義我們系統(tǒng)的認(rèn)證登錄流程,下面,我就一步一步在SpringSecurity 自定義認(rèn)證登錄,以手機(jī)驗(yàn)證碼登錄為例
1-自定義用戶對(duì)象
Spring Security 中定義了 UserDetails 接口來(lái)規(guī)范開(kāi)發(fā)者自定義的用戶對(duì)象,我們自定義對(duì)象直接實(shí)現(xiàn)這個(gè)接口,然后定義自己的對(duì)象屬性即可
/** * 自定義用戶角色 */ @Data public class PhoneUserDetails implements UserDetails { public static final String ACCOUNT_ACTIVE_STATUS = "ACTIVE"; public static final Integer NOT_EXPIRED = 0; private String userId; private String userName; private String phone; private String status; private Integer isExpired; @Override public Collection<? extends GrantedAuthority> getAuthorities() { Collection<GrantedAuthority> collection = new HashSet<>(); return collection; } @Override public String getPassword() { return null; } @Override public String getUsername() { return this.phone; } @Override public boolean isAccountNonExpired() { return NOT_EXPIRED.equals(isExpired); } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return ACCOUNT_ACTIVE_STATUS.equals(status); } }
自定義角色實(shí)現(xiàn)UserDetails接口方法時(shí),根據(jù)自己的需要來(lái)實(shí)現(xiàn)
2-自定義UserDetailsService
UserDetails是用來(lái)規(guī)范我們自定義用戶對(duì)象,而負(fù)責(zé)提供用戶數(shù)據(jù)源的接口是UserDetailsService,它提供了一個(gè)查詢用戶的方法,我們需要實(shí)現(xiàn)它來(lái)查詢用戶
@Service public class PhoneUserDetailsService implements UserDetailsService { public static final String USER_INFO_SUFFIX = "user:info:"; @Autowired private PhoneUserMapper phoneUserMapper; @Autowired private RedisTemplate<String,Object> redisTemplate; /** * 查找用戶 * @param username * @return * @throws UsernameNotFoundException */ @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { //先查詢緩存 String userKey = USER_INFO_SUFFIX + username; PhoneUserDetails cacheUserInfo = (PhoneUserDetails) redisTemplate.opsForValue().get(userKey); if (cacheUserInfo == null){ //緩存不存在,從數(shù)據(jù)庫(kù)查找用戶信息 PhoneUserDetails phoneUserDetails = phoneUserMapper.selectPhoneUserByPhone(username); if (phoneUserDetails == null){ throw new UsernameNotFoundException("用戶不存在"); } //加入緩存 redisTemplate.opsForValue().set(userKey,phoneUserDetails); return phoneUserDetails; } return cacheUserInfo; } }
3-自定義Authentication
在SpringSecurity認(rèn)證過(guò)程中,最核心的對(duì)象為Authentication,這個(gè)對(duì)象用于在認(rèn)證過(guò)程中存儲(chǔ)主體的各種基本信息(例如:用戶名,密碼等等)和主體的權(quán)限信息(例如,接口權(quán)限)。
我們可以通過(guò)繼承AbstractAuthenticationToken來(lái)自定義的Authentication對(duì)象,我們參考SpringSecurity自有的UsernamePasswordAuthenticationToken來(lái)實(shí)現(xiàn)自己的AbstractAuthenticationToken 實(shí)現(xiàn)類(lèi)
@Getter @Setter public class PhoneAuthenticationToken extends AbstractAuthenticationToken { private final Object principal; private Object credentials; /** * 可以自定義屬性 */ private String phone; /** * 創(chuàng)建一個(gè)未認(rèn)證的對(duì)象 * @param principal * @param credentials */ public PhoneAuthenticationToken(Object principal, Object credentials) { super(null); this.principal = principal; this.credentials = credentials; setAuthenticated(false); } public PhoneAuthenticationToken(Collection<? extends GrantedAuthority> authorities, Object principal, Object credentials) { super(authorities); this.principal = principal; this.credentials = credentials; // 必須使用super,因?yàn)槲覀円貙?xiě) super.setAuthenticated(true); } /** * 不能暴露Authenticated的設(shè)置方法,防止直接設(shè)置 * @param isAuthenticated * @throws IllegalArgumentException */ @Override public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException { Assert.isTrue(!isAuthenticated, "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead"); super.setAuthenticated(false); } /** * 用戶憑證,如密碼 * @return */ @Override public Object getCredentials() { return credentials; } /** * 被認(rèn)證主體的身份,如果是用戶名/密碼登錄,就是用戶名 * @return */ @Override public Object getPrincipal() { return principal; } }
因?yàn)槲覀兊尿?yàn)證碼是有時(shí)效性的,所以eraseCredentials 方法也沒(méi)必要重寫(xiě)了,無(wú)需擦除。主要是設(shè)置Authenticated屬性,Authenticated屬性代表是否已認(rèn)證
4-自定義AuthenticationProvider
AuthenticationProvider對(duì)于Spring Security來(lái)說(shuō)相當(dāng)于是身份驗(yàn)證的入口。通過(guò)向AuthenticationProvider提供認(rèn)證請(qǐng)求,我們可以得到認(rèn)證結(jié)果,進(jìn)而提供其他權(quán)限控制服務(wù)。
在Spring Security中,AuthenticationProvider是一個(gè)接口,其實(shí)現(xiàn)類(lèi)需要覆蓋authenticate(Authentication authentication)方法。當(dāng)用戶請(qǐng)求認(rèn)證時(shí),Authentication Provider就會(huì)嘗試對(duì)用戶提供的信息(Authentication對(duì)象里的信息)進(jìn)行認(rèn)證評(píng)估,并返回Authentication對(duì)象。通常一個(gè)provider對(duì)應(yīng)一種認(rèn)證方式,ProviderManager中可以包含多個(gè)AuthenticationProvider表示系統(tǒng)可以支持多種認(rèn)證方式。
Spring Security定義了AuthenticationProvider 接口來(lái)規(guī)范我們的AuthenticationProvider 實(shí)現(xiàn)類(lèi),AuthenticationProvider 接口只有兩個(gè)方法,源碼如下
public interface AuthenticationProvider { //身份認(rèn)證 Authentication authenticate(Authentication authentication) throws AuthenticationException; //是否支持傳入authentication類(lèi)型的認(rèn)證 boolean supports(Class<?> authentication); }
下面自定義我們的AuthenticationProvider,如果AuthenticationProvider認(rèn)證成功,它會(huì)返回一個(gè)完全有效的Authentication對(duì)象,其中authenticated屬性為true,已授權(quán)的權(quán)限列表(GrantedAuthority列表),以及用戶憑證。
/** * 手機(jī)驗(yàn)證碼認(rèn)證授權(quán)提供者 */ @Data public class PhoneAuthenticationProvider implements AuthenticationProvider { private RedisTemplate<String,Object> redisTemplate; private PhoneUserDetailsService phoneUserDetailsService; public static final String PHONE_CODE_SUFFIX = "phone:code:"; @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { //先將authentication轉(zhuǎn)為我們自定義的Authentication對(duì)象 PhoneAuthenticationToken authenticationToken = (PhoneAuthenticationToken) authentication; //校驗(yàn)參數(shù) Object principal = authentication.getPrincipal(); Object credentials = authentication.getCredentials(); if (principal == null || "".equals(principal.toString()) || credentials == null || "".equals(credentials.toString())){ throw new InternalAuthenticationServiceException("手機(jī)/手機(jī)驗(yàn)證碼為空!"); } //獲取手機(jī)號(hào)和驗(yàn)證碼 String phone = (String) authenticationToken.getPrincipal(); String code = (String) authenticationToken.getCredentials(); //查找手機(jī)用戶信息,驗(yàn)證用戶是否存在 UserDetails userDetails = phoneUserDetailsService.loadUserByUsername(phone); if (userDetails == null){ throw new InternalAuthenticationServiceException("用戶手機(jī)不存在!"); } String codeKey = PHONE_CODE_SUFFIX+phone; //手機(jī)用戶存在,驗(yàn)證手機(jī)驗(yàn)證碼是否正確 if (!redisTemplate.hasKey(codeKey)){ throw new InternalAuthenticationServiceException("驗(yàn)證碼不存在或已失效!"); } String realCode = (String) redisTemplate.opsForValue().get(codeKey); if (StringUtils.isBlank(realCode) || !realCode.equals(code)){ throw new InternalAuthenticationServiceException("驗(yàn)證碼錯(cuò)誤!"); } //返回認(rèn)證成功的對(duì)象 PhoneAuthenticationToken phoneAuthenticationToken = new PhoneAuthenticationToken(userDetails.getAuthorities(),phone,code); phoneAuthenticationToken.setPhone(phone); //details是一個(gè)泛型屬性,用于存儲(chǔ)關(guān)于認(rèn)證令牌的額外信息。其類(lèi)型是 Object,所以你可以存儲(chǔ)任何類(lèi)型的數(shù)據(jù)。這個(gè)屬性通常用于存儲(chǔ)與認(rèn)證相關(guān)的詳細(xì)信息,比如用戶的角色、IP地址、時(shí)間戳等。 phoneAuthenticationToken.setDetails(userDetails); return phoneAuthenticationToken; } /** * ProviderManager 選擇具體Provider時(shí)根據(jù)此方法判斷 * 判斷 authentication 是不是 SmsCodeAuthenticationToken 的子類(lèi)或子接口 */ @Override public boolean supports(Class<?> authentication) { //isAssignableFrom方法如果比較類(lèi)和被比較類(lèi)類(lèi)型相同,或者是其子類(lèi)、實(shí)現(xiàn)類(lèi),返回true return PhoneAuthenticationToken.class.isAssignableFrom(authentication); } }
5-自定義AbstractAuthenticationProcessingFilter
AbstractAuthenticationProcessingFilter是Spring Security中的一個(gè)重要的過(guò)濾器,用于處理用戶的身份驗(yàn)證。它是一個(gè)抽象類(lèi),提供了一些基本的身份驗(yàn)證功能,可以被子類(lèi)繼承和擴(kuò)展。該過(guò)濾器的主要作用是從請(qǐng)求中獲取用戶的身份認(rèn)證信息,并將其傳遞給AuthenticationManager進(jìn)行身份驗(yàn)證。如果身份驗(yàn)證成功,它將生成一個(gè)身份驗(yàn)證令牌,并將其傳遞給AuthenticationSuccessHandler進(jìn)行處理。如果身份驗(yàn)證失敗,它將生成一個(gè)身份驗(yàn)證異常,并將其傳遞給AuthenticationFailureHandler進(jìn)行處理。AbstractAuthenticationProcessingFilter還提供了一些其他的方法,如setAuthenticationManager()、setAuthenticationSuccessHandler()、setAuthenticationFailureHandler()等,可以用于定制身份認(rèn)證的處理方式。
我們需要自定義認(rèn)證流程,那么就需要繼承AbstractAuthenticationProcessingFilter這個(gè)抽象類(lèi)
Spring Security 的UsernamePasswordAuthenticationFilter也是繼承了AbstractAuthenticationProcessingFilter,我們可以參考實(shí)現(xiàn)自己的身份驗(yàn)證
public class PhoneVerificationCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter { /** * 參數(shù)名稱(chēng) */ public static final String USER_PHONE = "phone"; public static final String PHONE_CODE = "phoneCode"; private String userPhoneParameter = USER_PHONE; private String phoneCodeParameter = PHONE_CODE; /** * 是否只支持post請(qǐng)求 */ private boolean postOnly = true; /** * 通過(guò)構(gòu)造函數(shù),設(shè)置對(duì)哪些請(qǐng)求進(jìn)行過(guò)濾,如下設(shè)置,則只有接口為 /phone_login,請(qǐng)求方式為 POST的請(qǐng)求才會(huì)進(jìn)入邏輯 */ public PhoneVerificationCodeAuthenticationFilter(){ super(new RegexRequestMatcher("/phone_login","POST")); } /** * 認(rèn)證方法 * @param request * @param response * @return * @throws AuthenticationException * @throws IOException * @throws ServletException */ @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException { PhoneAuthenticationToken phoneAuthenticationToken; //請(qǐng)求方法類(lèi)型校驗(yàn) if (this.postOnly && !request.getMethod().equals("POST")) { throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod()); } //如果不是json參數(shù),從request獲取參數(shù) if (!request.getContentType().equals(MediaType.APPLICATION_JSON_UTF8_VALUE) && !request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE)) { String userPhone = request.getParameter(userPhoneParameter); String phoneCode = request.getParameter(phoneCodeParameter); phoneAuthenticationToken = new PhoneAuthenticationToken(userPhone,phoneCode); }else { //如果是json請(qǐng)求使用取參數(shù)邏輯,直接用map接收,也可以創(chuàng)建一個(gè)實(shí)體類(lèi)接收 Map<String, String> loginData = new HashMap<>(2); try { loginData = JSONObject.parseObject(request.getInputStream(), Map.class); } catch (IOException e) { throw new InternalAuthenticationServiceException("請(qǐng)求參數(shù)異常"); } // 獲得請(qǐng)求參數(shù) String userPhone = loginData.get(userPhoneParameter); String phoneCode = loginData.get(phoneCodeParameter); phoneAuthenticationToken = new PhoneAuthenticationToken(userPhone,phoneCode); } phoneAuthenticationToken.setDetails(authenticationDetailsSource.buildDetails(request)); return this.getAuthenticationManager().authenticate(phoneAuthenticationToken); } }
6-自定義認(rèn)證成功和失敗的處理類(lèi)
pringSecurity處理成功和失敗一般是進(jìn)行頁(yè)面跳轉(zhuǎn),但是在前后端分離的架構(gòu)下,前后端的交互一般是通過(guò)json進(jìn)行交互,不需要后端重定向或者跳轉(zhuǎn),只需要返回我們的登陸信息即可。
這就要實(shí)現(xiàn)我們的認(rèn)證成功和失敗處理類(lèi)
認(rèn)證成功接口:AuthenticationSuccessHandler,只有一個(gè)onAuthenticationSuccess認(rèn)證成功處理方法
認(rèn)證失敗接口:AuthenticationFailureHandler,只有一個(gè)onAuthenticationFailure認(rèn)證失敗處理方法
我們實(shí)現(xiàn)相應(yīng)接口,在方法中定義好我們的處理邏輯即可
@Component public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler { /** * 登錄成功處理 * @param httpServletRequest * @param httpServletResponse * @param authentication * @throws IOException * @throws ServletException */ @Override public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException { httpServletResponse.setContentType("application/json;charset=utf-8"); Map<String, Object> resp = new HashMap<>(); resp.put("status", 200); resp.put("msg", "登錄成功!"); resp.put("token", new UUIDGenerator().next()); String s = JSONObject.toJSONString(resp); httpServletResponse.getWriter().write(s); } } @Slf4j @Component public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler { /** * 登錄失敗處理 * @param httpServletRequest * @param httpServletResponse * @param exception * @throws IOException * @throws ServletException */ @Override public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException exception) throws IOException, ServletException { httpServletResponse.setContentType("application/json;charset=utf-8"); Map<String, Object> resp = new HashMap<>(); resp.put("status", 500); resp.put("msg", "登錄失敗!" ); String s = JSONObject.toJSONString(resp); log.error("登錄異常:",exception); httpServletResponse.getWriter().write(s); } }
7-修改配置類(lèi)
想要應(yīng)用自定義的 AuthenticationProvider 和 AbstractAuthenticationProcessingFilter,還需在WebSecurityConfigurerAdapter 配置類(lèi)進(jìn)行配置。
@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private RedisTemplate<String, Object> redisTemplate; @Autowired private PhoneUserDetailsService phoneUserDetailsService; @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests().anyRequest().authenticated() .and() .formLogin().successHandler(new CustomAuthenticationSuccessHandler()).permitAll() .and() .csrf().disable(); //添加自定義過(guò)濾器 PhoneVerificationCodeAuthenticationFilter phoneVerificationCodeAuthenticationFilter = new PhoneVerificationCodeAuthenticationFilter(); //設(shè)置過(guò)濾器認(rèn)證成功和失敗的處理類(lèi) phoneVerificationCodeAuthenticationFilter.setAuthenticationSuccessHandler(new CustomAuthenticationSuccessHandler()); phoneVerificationCodeAuthenticationFilter.setAuthenticationFailureHandler(new CustomAuthenticationFailureHandler()); //設(shè)置認(rèn)證管理器 phoneVerificationCodeAuthenticationFilter.setAuthenticationManager(authenticationManager()); //addFilterBefore方法用于將自定義的過(guò)濾器添加到過(guò)濾器鏈中,并指定該過(guò)濾器在哪個(gè)已存在的過(guò)濾器之前執(zhí)行 http.addFilterBefore(phoneVerificationCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { // 采用密碼授權(quán)模式需要顯式配置AuthenticationManager return super.authenticationManagerBean(); } /** * * @param auth 認(rèn)證管理器 * @throws Exception */ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { //添加自定義認(rèn)證提供者 auth.authenticationProvider(phoneAuthenticationProvider()); } /** * 手機(jī)驗(yàn)證碼登錄的認(rèn)證提供者 * @return */ @Bean public PhoneAuthenticationProvider phoneAuthenticationProvider(){ PhoneAuthenticationProvider phoneAuthenticationProvider = new PhoneAuthenticationProvider(); phoneAuthenticationProvider.setRedisTemplate(redisTemplate); phoneAuthenticationProvider.setPhoneUserDetailsService(phoneUserDetailsService); return phoneAuthenticationProvider; } }
在Spring Security框架中,addFilterBefore方法用于將自定義的過(guò)濾器添加到過(guò)濾器鏈中,并指定該過(guò)濾器在哪個(gè)已存在的過(guò)濾器之前執(zhí)行。還有一個(gè)addFilterAfter方法可以將自定義過(guò)濾器添加到指定過(guò)濾器之后執(zhí)行。
8-測(cè)試
完成上面的操作之后,我們就可以測(cè)試下新的登錄方式是否生效了。我這里直接使用postman進(jìn)行登錄請(qǐng)求
到此這篇關(guān)于SpringSecurity 自定義認(rèn)證登錄的項(xiàng)目實(shí)踐的文章就介紹到這了,更多相關(guān)SpringSecurity 自定義認(rèn)證登錄內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- Spring Security 自定義短信登錄認(rèn)證的實(shí)現(xiàn)
- Springboot+Spring Security實(shí)現(xiàn)前后端分離登錄認(rèn)證及權(quán)限控制的示例代碼
- Java SpringSecurity+JWT實(shí)現(xiàn)登錄認(rèn)證
- SpringBoot security安全認(rèn)證登錄的實(shí)現(xiàn)方法
- SpringSecurity實(shí)現(xiàn)前后端分離登錄token認(rèn)證詳解
- Springboot整合SpringSecurity實(shí)現(xiàn)登錄認(rèn)證和鑒權(quán)全過(guò)程
- springsecurity實(shí)現(xiàn)用戶登錄認(rèn)證快速使用示例代碼(前后端分離項(xiàng)目)
- Spring Security實(shí)現(xiàn)登錄認(rèn)證實(shí)戰(zhàn)教程
- spring security登錄認(rèn)證授權(quán)的項(xiàng)目實(shí)踐
相關(guān)文章
java通過(guò)JFrame做一個(gè)登錄系統(tǒng)的界面完整代碼示例
這篇文章主要介紹了java通過(guò)JFrame做一個(gè)登錄系統(tǒng)的界面完整代碼示例,具有一定借鑒價(jià)值,需要的朋友可以參考下。2017-12-12Spring?Bean自動(dòng)裝配入門(mén)到精通
自動(dòng)裝配是使用spring滿足bean依賴(lài)的一種方法,spring會(huì)在應(yīng)用上下文中為某個(gè)bean尋找其依賴(lài)的bean,Spring中bean有三種裝配機(jī)制,分別是:在xml中顯式配置、在java中顯式配置、隱式的bean發(fā)現(xiàn)機(jī)制和自動(dòng)裝配2022-08-08調(diào)用Process.waitfor導(dǎo)致的進(jìn)程掛起問(wèn)題及解決
這篇文章主要介紹了調(diào)用Process.waitfor導(dǎo)致的進(jìn)程掛起問(wèn)題及解決,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-12-12springboot+swagger2.10.5+mybatis-plus 入門(mén)詳解
這篇文章主要介紹了springboot+swagger2.10.5+mybatis-plus 入門(mén),本文通過(guò)實(shí)例圖文相結(jié)合給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-12-12從匯編碼分析java對(duì)象的創(chuàng)建過(guò)程(推薦)
這篇文章主要介紹了從匯編碼分析java對(duì)象的創(chuàng)建過(guò)程,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-03-03SpringBoot+Vue跨域配置(CORS)問(wèn)題得解決過(guò)程
在使用 Spring Boot 和 Vue 開(kāi)發(fā)前后端分離的項(xiàng)目時(shí),跨域資源共享(CORS)問(wèn)題是一個(gè)常見(jiàn)的挑戰(zhàn),接下來(lái),我將分享我是如何一步步解決這個(gè)問(wèn)題的,包括中間的一些試錯(cuò)過(guò)程,希望能夠幫助到正在經(jīng)歷類(lèi)似問(wèn)題的你2024-08-08Java日期操作方法工具類(lèi)實(shí)例【包含日期比較大小,相加減,判斷,驗(yàn)證,獲取年份等】
這篇文章主要介紹了Java日期操作方法工具類(lèi),結(jié)合完整實(shí)例形式分析了java針對(duì)日期的各種常見(jiàn)操作,包括日期比較大小,相加減,判斷,驗(yàn)證,獲取年份、天數(shù)、星期等,需要的朋友可以參考下2017-11-11mybatisplus下劃線駝峰轉(zhuǎn)換的問(wèn)題解決
在mybatis-plus中,下劃線-駝峰自動(dòng)轉(zhuǎn)換可能導(dǎo)致帶下劃線的字段查詢結(jié)果為null,本文就來(lái)介紹一下mybatisplus下劃線駝峰轉(zhuǎn)換的問(wèn)題解決,感興趣的可以了解一下2024-10-10