SpringSecurity 自定義認(rèn)證登錄的項目實踐
前言
現(xiàn)在登錄方式越來越多,傳統(tǒng)的賬號密碼登錄已經(jīng)不能滿足我們的需求??赡芪覀冞€需要手機驗證碼登錄,郵箱驗證碼登錄,一鍵登錄等。這時候就需要我們自定義我們系統(tǒng)的認(rèn)證登錄流程,下面,我就一步一步在SpringSecurity 自定義認(rèn)證登錄,以手機驗證碼登錄為例
1-自定義用戶對象
Spring Security 中定義了 UserDetails 接口來規(guī)范開發(fā)者自定義的用戶對象,我們自定義對象直接實現(xiàn)這個接口,然后定義自己的對象屬性即可
/**
* 自定義用戶角色
*/
@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);
}
}
自定義角色實現(xiàn)UserDetails接口方法時,根據(jù)自己的需要來實現(xiàn)
2-自定義UserDetailsService
UserDetails是用來規(guī)范我們自定義用戶對象,而負(fù)責(zé)提供用戶數(shù)據(jù)源的接口是UserDetailsService,它提供了一個查詢用戶的方法,我們需要實現(xiàn)它來查詢用戶
@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ù)庫查找用戶信息
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)證過程中,最核心的對象為Authentication,這個對象用于在認(rèn)證過程中存儲主體的各種基本信息(例如:用戶名,密碼等等)和主體的權(quán)限信息(例如,接口權(quán)限)。
我們可以通過繼承AbstractAuthenticationToken來自定義的Authentication對象,我們參考SpringSecurity自有的UsernamePasswordAuthenticationToken來實現(xiàn)自己的AbstractAuthenticationToken 實現(xiàn)類
@Getter
@Setter
public class PhoneAuthenticationToken extends AbstractAuthenticationToken {
private final Object principal;
private Object credentials;
/**
* 可以自定義屬性
*/
private String phone;
/**
* 創(chuàng)建一個未認(rèn)證的對象
* @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,因為我們要重寫
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;
}
}
因為我們的驗證碼是有時效性的,所以eraseCredentials 方法也沒必要重寫了,無需擦除。主要是設(shè)置Authenticated屬性,Authenticated屬性代表是否已認(rèn)證
4-自定義AuthenticationProvider
AuthenticationProvider對于Spring Security來說相當(dāng)于是身份驗證的入口。通過向AuthenticationProvider提供認(rèn)證請求,我們可以得到認(rèn)證結(jié)果,進而提供其他權(quán)限控制服務(wù)。
在Spring Security中,AuthenticationProvider是一個接口,其實現(xiàn)類需要覆蓋authenticate(Authentication authentication)方法。當(dāng)用戶請求認(rèn)證時,Authentication Provider就會嘗試對用戶提供的信息(Authentication對象里的信息)進行認(rèn)證評估,并返回Authentication對象。通常一個provider對應(yīng)一種認(rèn)證方式,ProviderManager中可以包含多個AuthenticationProvider表示系統(tǒng)可以支持多種認(rèn)證方式。
Spring Security定義了AuthenticationProvider 接口來規(guī)范我們的AuthenticationProvider 實現(xiàn)類,AuthenticationProvider 接口只有兩個方法,源碼如下
public interface AuthenticationProvider {
//身份認(rèn)證
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
//是否支持傳入authentication類型的認(rèn)證
boolean supports(Class<?> authentication);
}
下面自定義我們的AuthenticationProvider,如果AuthenticationProvider認(rèn)證成功,它會返回一個完全有效的Authentication對象,其中authenticated屬性為true,已授權(quán)的權(quán)限列表(GrantedAuthority列表),以及用戶憑證。
/**
* 手機驗證碼認(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對象
PhoneAuthenticationToken authenticationToken = (PhoneAuthenticationToken) authentication;
//校驗參數(shù)
Object principal = authentication.getPrincipal();
Object credentials = authentication.getCredentials();
if (principal == null || "".equals(principal.toString()) || credentials == null || "".equals(credentials.toString())){
throw new InternalAuthenticationServiceException("手機/手機驗證碼為空!");
}
//獲取手機號和驗證碼
String phone = (String) authenticationToken.getPrincipal();
String code = (String) authenticationToken.getCredentials();
//查找手機用戶信息,驗證用戶是否存在
UserDetails userDetails = phoneUserDetailsService.loadUserByUsername(phone);
if (userDetails == null){
throw new InternalAuthenticationServiceException("用戶手機不存在!");
}
String codeKey = PHONE_CODE_SUFFIX+phone;
//手機用戶存在,驗證手機驗證碼是否正確
if (!redisTemplate.hasKey(codeKey)){
throw new InternalAuthenticationServiceException("驗證碼不存在或已失效!");
}
String realCode = (String) redisTemplate.opsForValue().get(codeKey);
if (StringUtils.isBlank(realCode) || !realCode.equals(code)){
throw new InternalAuthenticationServiceException("驗證碼錯誤!");
}
//返回認(rèn)證成功的對象
PhoneAuthenticationToken phoneAuthenticationToken = new PhoneAuthenticationToken(userDetails.getAuthorities(),phone,code);
phoneAuthenticationToken.setPhone(phone);
//details是一個泛型屬性,用于存儲關(guān)于認(rèn)證令牌的額外信息。其類型是 Object,所以你可以存儲任何類型的數(shù)據(jù)。這個屬性通常用于存儲與認(rèn)證相關(guān)的詳細信息,比如用戶的角色、IP地址、時間戳等。
phoneAuthenticationToken.setDetails(userDetails);
return phoneAuthenticationToken;
}
/**
* ProviderManager 選擇具體Provider時根據(jù)此方法判斷
* 判斷 authentication 是不是 SmsCodeAuthenticationToken 的子類或子接口
*/
@Override
public boolean supports(Class<?> authentication) {
//isAssignableFrom方法如果比較類和被比較類類型相同,或者是其子類、實現(xiàn)類,返回true
return PhoneAuthenticationToken.class.isAssignableFrom(authentication);
}
}
5-自定義AbstractAuthenticationProcessingFilter
AbstractAuthenticationProcessingFilter是Spring Security中的一個重要的過濾器,用于處理用戶的身份驗證。它是一個抽象類,提供了一些基本的身份驗證功能,可以被子類繼承和擴展。該過濾器的主要作用是從請求中獲取用戶的身份認(rèn)證信息,并將其傳遞給AuthenticationManager進行身份驗證。如果身份驗證成功,它將生成一個身份驗證令牌,并將其傳遞給AuthenticationSuccessHandler進行處理。如果身份驗證失敗,它將生成一個身份驗證異常,并將其傳遞給AuthenticationFailureHandler進行處理。AbstractAuthenticationProcessingFilter還提供了一些其他的方法,如setAuthenticationManager()、setAuthenticationSuccessHandler()、setAuthenticationFailureHandler()等,可以用于定制身份認(rèn)證的處理方式。
我們需要自定義認(rèn)證流程,那么就需要繼承AbstractAuthenticationProcessingFilter這個抽象類
Spring Security 的UsernamePasswordAuthenticationFilter也是繼承了AbstractAuthenticationProcessingFilter,我們可以參考實現(xiàn)自己的身份驗證
public class PhoneVerificationCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
/**
* 參數(shù)名稱
*/
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請求
*/
private boolean postOnly = true;
/**
* 通過構(gòu)造函數(shù),設(shè)置對哪些請求進行過濾,如下設(shè)置,則只有接口為 /phone_login,請求方式為 POST的請求才會進入邏輯
*/
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;
//請求方法類型校驗
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請求使用取參數(shù)邏輯,直接用map接收,也可以創(chuàng)建一個實體類接收
Map<String, String> loginData = new HashMap<>(2);
try {
loginData = JSONObject.parseObject(request.getInputStream(), Map.class);
} catch (IOException e) {
throw new InternalAuthenticationServiceException("請求參數(shù)異常");
}
// 獲得請求參數(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)證成功和失敗的處理類
pringSecurity處理成功和失敗一般是進行頁面跳轉(zhuǎn),但是在前后端分離的架構(gòu)下,前后端的交互一般是通過json進行交互,不需要后端重定向或者跳轉(zhuǎn),只需要返回我們的登陸信息即可。
這就要實現(xiàn)我們的認(rèn)證成功和失敗處理類
認(rèn)證成功接口:AuthenticationSuccessHandler,只有一個onAuthenticationSuccess認(rèn)證成功處理方法
認(rèn)證失敗接口:AuthenticationFailureHandler,只有一個onAuthenticationFailure認(rèn)證失敗處理方法
我們實現(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-修改配置類
想要應(yīng)用自定義的 AuthenticationProvider 和 AbstractAuthenticationProcessingFilter,還需在WebSecurityConfigurerAdapter 配置類進行配置。
@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();
//添加自定義過濾器
PhoneVerificationCodeAuthenticationFilter phoneVerificationCodeAuthenticationFilter = new PhoneVerificationCodeAuthenticationFilter();
//設(shè)置過濾器認(rèn)證成功和失敗的處理類
phoneVerificationCodeAuthenticationFilter.setAuthenticationSuccessHandler(new CustomAuthenticationSuccessHandler());
phoneVerificationCodeAuthenticationFilter.setAuthenticationFailureHandler(new CustomAuthenticationFailureHandler());
//設(shè)置認(rèn)證管理器
phoneVerificationCodeAuthenticationFilter.setAuthenticationManager(authenticationManager());
//addFilterBefore方法用于將自定義的過濾器添加到過濾器鏈中,并指定該過濾器在哪個已存在的過濾器之前執(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());
}
/**
* 手機驗證碼登錄的認(rèn)證提供者
* @return
*/
@Bean
public PhoneAuthenticationProvider phoneAuthenticationProvider(){
PhoneAuthenticationProvider phoneAuthenticationProvider = new PhoneAuthenticationProvider();
phoneAuthenticationProvider.setRedisTemplate(redisTemplate);
phoneAuthenticationProvider.setPhoneUserDetailsService(phoneUserDetailsService);
return phoneAuthenticationProvider;
}
}
在Spring Security框架中,addFilterBefore方法用于將自定義的過濾器添加到過濾器鏈中,并指定該過濾器在哪個已存在的過濾器之前執(zhí)行。還有一個addFilterAfter方法可以將自定義過濾器添加到指定過濾器之后執(zhí)行。
8-測試
完成上面的操作之后,我們就可以測試下新的登錄方式是否生效了。我這里直接使用postman進行登錄請求

到此這篇關(guān)于SpringSecurity 自定義認(rèn)證登錄的項目實踐的文章就介紹到這了,更多相關(guān)SpringSecurity 自定義認(rèn)證登錄內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- Spring Security 自定義短信登錄認(rèn)證的實現(xiàn)
- Springboot+Spring Security實現(xiàn)前后端分離登錄認(rèn)證及權(quán)限控制的示例代碼
- Java SpringSecurity+JWT實現(xiàn)登錄認(rèn)證
- SpringBoot security安全認(rèn)證登錄的實現(xiàn)方法
- SpringSecurity實現(xiàn)前后端分離登錄token認(rèn)證詳解
- Springboot整合SpringSecurity實現(xiàn)登錄認(rèn)證和鑒權(quán)全過程
- springsecurity實現(xiàn)用戶登錄認(rèn)證快速使用示例代碼(前后端分離項目)
- Spring Security實現(xiàn)登錄認(rèn)證實戰(zhàn)教程
- spring security登錄認(rèn)證授權(quán)的項目實踐
相關(guān)文章
詳解springboot 使用c3p0數(shù)據(jù)庫連接池的方法
本篇文章主要介紹了springboot 使用c3p0數(shù)據(jù)庫連接池的方法,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-09-09
springboot 啟動如何修改application.properties的參數(shù)
這篇文章主要介紹了springboot 啟動如何修改application.properties的參數(shù)方法,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-08-08
Spring Cloud Gateway 使用JWT工具類做用戶登錄校驗功能
這篇文章主要介紹了Spring Cloud Gateway 使用JWT工具類做用戶登錄校驗的示例代碼,本文給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-01-01
詳解Java實現(xiàn)JSONArray轉(zhuǎn)Map的三種實現(xiàn)方式
本文主要介紹了Java實現(xiàn)JSONArray轉(zhuǎn)Map的三種實現(xiàn)方式,本文只是自己常用的三種,文中通過示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-03-03
Mybatis通過Mapper代理連接數(shù)據(jù)庫的方法
這篇文章主要介紹了Mybatis通過Mapper代理連接數(shù)據(jù)庫的方法,本文通過實例代碼給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-11-11
基于rocketmq的有序消費模式和并發(fā)消費模式的區(qū)別說明
這篇文章主要介紹了基于rocketmq的有序消費模式和并發(fā)消費模式的區(qū)別說明,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-06-06
springboot開啟Bean數(shù)據(jù)校驗功能
這篇文章主要介紹了springboot開啟Bean數(shù)據(jù)校驗功能,通過啟用Bean屬性校驗導(dǎo)入JSR303與Hibernate校驗框架坐標(biāo),使用@Validated注解啟用校驗功能,需要的朋友可以參考下2023-10-10

