日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 前端技术 > javascript >内容正文

javascript

手把手带你在集成SpringSecurity的SpringBoot应用中添加短信验证码登录认证功能

發布時間:2023/12/31 javascript 27 豆豆
生活随笔 收集整理的這篇文章主要介紹了 手把手带你在集成SpringSecurity的SpringBoot应用中添加短信验证码登录认证功能 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

本文目錄

    • 前言
    • 1 自定義AuthenticationToken類
    • 2 自定義AuthenticationProvider類
    • 3 自定義MobilePhoneAuthenticationFilter
    • 3 修改UserService類
    • 5 修改短信服務sendLoginVeryCodeMessage方法
    • 6 修改WebSecurityConfig配置類
    • 7 驗證效果

前言

在上一篇文章一文理清SpringSecurity中基于用于名密碼的登錄認證流程中筆者有詳細地介紹了Spring Security登錄認證的流程,也為我們在工作中面需要實現自定義的登錄認證如手機號+短信驗證碼、郵箱地址+郵箱驗證碼以及第三方登錄認證等方式的擴展做好了準備。那么本文,筆者就手把手帶大家實現在集成了Spring Security的SpringBoot項目中如何增加一種手機號+短信驗證碼的方式實現登錄認證。

最新為了節約搭建項目的時間成本,本文功能的實現在筆者之前改造過的開源項目 blogserver的基礎上進行,項目代碼地址筆者會在文末提供,希望讀者們都能花個5分鐘左右堅持看到文末。

1 自定義AuthenticationToken類

我們自定義的MobilePhoneAuthenticationToken類繼承自AbstractAuthenticationToken類,主要提供一個帶參構造方法并重寫getCredentials、getPrincipal、setAuthenticated、eraseCredential和getName`等方法

public class MobilePhoneAuthenticationToken extends AbstractAuthenticationToken {// 登錄身份,這里是手機號private Object principal;// 登錄憑證,這里是短信驗證碼private Object credentials;/*** 構造方法* @param authorities 權限集合* @param principal 登錄身份* @param credentials 登錄憑據*/public MobilePhoneAuthenticationToken(Collection<? extends GrantedAuthority> authorities, Object principal, Object credentials) {super(authorities);this.principal = principal;this.credentials = credentials;super.setAuthenticated(true);}@Overridepublic Object getCredentials() {return credentials;}@Overridepublic Object getPrincipal() {return principal;}// 不允許通過set方法設置認證標識@Overridepublic void setAuthenticated(boolean authenticated) {if (authenticated) {throw new IllegalArgumentException("Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");}super.setAuthenticated(false);}// 擦除登錄憑據@Overridepublic void eraseCredentials() {super.eraseCredentials();credentials = null;}// 獲取認證token的名字@Overridepublic String getName() {return "mobilePhoneAuthenticationToken";} }

2 自定義AuthenticationProvider類

我們自定義的MobilePhoneAuthenticationProvider類的時候 我們參照了AbstractUserDetailsAuthenticationProvider類的源碼, 同時實現了AuthenticationProvider、InitializingBean和MessageSourceAware等三個接口

同時為了實現手機號+短信驗證碼登錄認證的功能,我們在這個類中添加了UserService和RedisTemplate兩個類屬性,作為MobilePhoneAuthenticationProvider類的兩個構造參數

該類的編碼完成后的源碼如下:

public class MobilePhoneAuthenticationProvider implements AuthenticationProvider, InitializingBean, MessageSourceAware {private UserService userService;private RedisTemplate redisTemplate;private boolean forcePrincipalAsString = false;private static final Logger logger = LoggerFactory.getLogger(MobilePhoneAuthenticationProvider.class);protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();private UserDetailsChecker userDetailsChecker = new AccountStatusUserDetailsChecker();public MobilePhoneAuthenticationProvider(UserService userService, RedisTemplate redisTemplate) {this.userService = userService;this.redisTemplate = redisTemplate;}/*** 認證方法* @param authentication 認證token* @return successAuthenticationToken* @throws AuthenticationException 認證異常*/@Overridepublic Authentication authenticate(Authentication authentication) throws AuthenticationException {// 首先判斷authentication參數必須是一個MobilePhoneAuthenticationToken類型對象Assert.isInstanceOf(MobilePhoneAuthenticationToken.class, authentication,()-> this.messages.getMessage("MobilePhoneAuthenticationProvider.onlySupports", "Only MobilePhoneAuthenticationToken is supported"));// 獲取authentication參數的principal屬性作為手機號String phoneNo = authentication.getPrincipal().toString();if (StringUtils.isEmpty(phoneNo)) {logger.error("phoneNo cannot be null");throw new BadCredentialsException("phoneNo cannot be null");}// 獲取authentication參數的credentials屬性作為短信驗證碼String phoneCode = authentication.getCredentials().toString();if (StringUtils.isEmpty(phoneCode)) {logger.error("phoneCode cannot be null");throw new BadCredentialsException("phoneCode cannot be null");}try {// 調用userService服務根據手機號查詢用戶信息CustomUser user = (CustomUser) userService.loadUserByPhoneNum(Long.parseLong(phoneNo));// 校驗用戶賬號是否過期、是否被鎖住、是否有效等屬性userDetailsChecker.check(user);// 根據手機號組成的key值去redis緩存中查詢發送短信驗證碼時存儲的驗證碼String storedPhoneCode = (String) redisTemplate.opsForValue().get("loginVerifyCode:"+phoneNo);if (storedPhoneCode==null) {logger.error("phoneCode is expired");throw new BadCredentialsException("phoneCode is expired");}// 用戶登錄攜帶的短信驗證碼與redis中根據手機號查詢出來的登錄認證短信驗證碼不一致則拋出驗證碼錯誤異常if (!phoneCode.equals(storedPhoneCode)) {logger.error("the phoneCode is not correct");throw new BadCredentialsException("the phoneCode is not correct");}// 把完成的用戶信息賦值給組成返回認證token中的principal屬性值Object principalToReturn = user;// 如果強制把用戶信息轉成字符串,則只返回用戶的手機號碼if(isForcePrincipalAsString()) {principalToReturn = user.getPhoneNum();}// 認證成功則返回一個MobilePhoneAuthenticationToken實例對象,principal屬性為較為完整的用戶信息MobilePhoneAuthenticationToken successAuthenticationToken = new MobilePhoneAuthenticationToken(user.getAuthorities(), principalToReturn, phoneCode);return successAuthenticationToken;} catch (UsernameNotFoundException e) {// 用戶手機號不存在,如果用戶已注冊提示用戶先去個人信息頁面添加手機號碼信息,否則提示用戶使用手機號注冊成為用戶后再登錄logger.error("user " + phoneNo + "not found, if you have been register as a user, please goto the page of edit user information to add you phone number, " +"else you must register as a user use you phone number");throw new BadCredentialsException("user " + phoneNo + "not found, if you have been register as a user, please goto the page of edit user information to add you phone number, " +"else you must register as a user use you phone number");} catch (NumberFormatException e) {logger.error("invalid phoneNo, due it is not a number");throw new BadCredentialsException("invalid phoneNo, due do phoneNo is not a number");}}/*** 只支持自定義的MobilePhoneAuthenticationToken類的認證*/@Overridepublic boolean supports(Class<?> aClass) {return aClass.isAssignableFrom(MobilePhoneAuthenticationToken.class);}@Overridepublic void afterPropertiesSet() throws Exception {Assert.notNull(this.messages, "A message source must be set");Assert.notNull(this.redisTemplate, "A RedisTemplate must be set");Assert.notNull(this.userService, "A UserDetailsService must be set");}@Overridepublic void setMessageSource(MessageSource messageSource) {this.messages = new MessageSourceAccessor(messageSource);}public void setForcePrincipalAsString(boolean forcePrincipalAsString) {this.forcePrincipalAsString = forcePrincipalAsString;}public boolean isForcePrincipalAsString() {return forcePrincipalAsString;} }

在這個自定義的認證器類中主要在authenticate方法中完成自定義的認證邏輯,最后認證成功之后返回一個新的

MobilePhoneAuthenticationToken對象,principal屬性為認證通過后的用戶詳細信息。

3 自定義MobilePhoneAuthenticationFilter

該類我們參照UsernamePasswordAuthenticationFilter類的源碼實現一個專門用于手機號+驗證碼登錄認證的認證過濾器,它的源碼如下,我們主要在attemptAuthentication方法中完成從HttpServletRequest類型請求參數中提取手機號和短信驗證碼等請求參數。然后組裝成一個MobilePhoneAuthenticationToken對象,用于調用this.getAuthenticationManager().authenticate方法時作為參數傳入。

實現重寫attemptAuthentication方法后的MobilePhoneAuthenticationFilter類的源碼如下:

/*** 自定義手機登錄認證過濾器*/ public class MobilePhoneAuthenticationFilter extends AbstractAuthenticationProcessingFilter {public static final String SPRING_SECURITY_PHONE_NO_KEY = "phoneNo";public static final String SPRING_SECURITY_PHONE_CODE_KEY = "phoneCode";private String phoneNoParameter = SPRING_SECURITY_PHONE_NO_KEY;private String phoneCodeParameter = SPRING_SECURITY_PHONE_CODE_KEY;private boolean postOnly = true;public MobilePhoneAuthenticationFilter(String defaultFilterProcessesUrl) {super(defaultFilterProcessesUrl);}public MobilePhoneAuthenticationFilter(RequestMatcher requiresAuthenticationRequestMatcher) {super(requiresAuthenticationRequestMatcher);}@Overridepublic Authentication attemptAuthentication(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws AuthenticationException, IOException, ServletException {if (postOnly && !httpServletRequest.getMethod().equals("POST")) {throw new AuthenticationServiceException("Authentication method not supported: " + httpServletRequest.getMethod());}String phoneNo = obtainPhoneNo(httpServletRequest);if (phoneNo==null) {phoneNo = "";} else {phoneNo = phoneNo.trim();}String phoneCode = obtainPhoneCode(httpServletRequest);if (phoneCode==null) {phoneCode = "";} else {phoneCode = phoneCode.trim();}MobilePhoneAuthenticationToken authRequest = new MobilePhoneAuthenticationToken(new ArrayList<>(), phoneNo, phoneCode);this.setDetails(httpServletRequest, authRequest);return this.getAuthenticationManager().authenticate(authRequest);}@Nullableprotected String obtainPhoneNo(HttpServletRequest request) {return request.getParameter(phoneNoParameter);}@Nullableprotected String obtainPhoneCode(HttpServletRequest request) {return request.getParameter(phoneCodeParameter);}protected void setDetails(HttpServletRequest request, MobilePhoneAuthenticationToken authRequest) {authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));} }

3 修改UserService類

UserService類主要在用來查詢用戶自定義信息,我們在該類中添加根據手機號查詢用戶信息方法。注意如果用戶表中沒有手機號碼字段,需要給表新增一個存儲手機號碼的字段,列類型為bigint, 實體類中該字段為Long類型

UserService類中實現根據用戶手機號查詢用戶信息的實現代碼如下:

@Service @Transactional public class UserService implements CustomUserDetailsService {@ResourceUserMapper userMapper;@ResourceRolesMapper rolesMapper;@ResourcePasswordEncoder passwordEncoder;private static final Logger logger = LoggerFactory.getLogger(UserService.class);/*** 根據用戶手機號查詢用戶詳細信息* @param phoneNum 手機號* @return customUser* @throws UsernameNotFoundException*/@Overridepublic UserDetails loadUserByPhoneNum(Long phoneNum) throws UsernameNotFoundException {logger.info("用戶登錄認證, phoneNum={}", phoneNum);UserDTO userDTO = userMapper.loadUserByPhoneNum(phoneNum);if (userDTO == null) {// 拋UsernameNotFoundException異常throw new UsernameNotFoundException("user " + phoneNum + " not exist!");}CustomUser customUser = convertUserDTO2CustomUser(userDTO);return customUser;}/*** UserDTO轉CustomUser對象* @param userDTO* @return user*/private CustomUser convertUserDTO2CustomUser(UserDTO userDTO) {//查詢用戶的角色信息,并返回存入user中List<Role> roles = rolesMapper.getRolesByUid(userDTO.getId());// 權限大的角色排在前面roles.sort(Comparator.comparing(Role::getId));CustomUser user = new CustomUser(userDTO.getUsername(), userDTO.getPassword(),userDTO.getEnabled()==1, true, true,true, new ArrayList<>());user.setId(userDTO.getId());user.setNickname(userDTO.getNickname());user.setPhoneNum(userDTO.getPhoneNum());user.setEmail(userDTO.getEmail());user.setUserface(userDTO.getUserface());user.setRegTime(userDTO.getRegTime());user.setUpdateTime(userDTO.getUpdateTime());user.setRoles(roles);user.setCurrentRole(roles.get(0));return user;}}

UserDTO和CustomUser兩個實體類源碼如下:

public class UserDTO implements Serializable {private Long id;private String username;private String password;private String nickname;private Long phoneNum;// 有效標識:0-無效;1-有效private int enabled;private String email;private String userface;private Timestamp regTime;private Timestamp updateTime;// ......省略各個屬性的set和get方法 } public class CustomUser extends User {private Long id;private String nickname;private Long phoneNum;private List<Role> roles;// 當前角色private Role currentRole;private String email;private String userface;@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8")private Date regTime;@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8")private Date updateTime;public CustomUser(String username, String password, Collection<? extends GrantedAuthority> authorities) {super(username, password, authorities);}public CustomUser(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);}@Override@JsonIgnorepublic List<GrantedAuthority> getAuthorities() {List<GrantedAuthority> authorities = new ArrayList<>();for (Role role : roles) {authorities.add(new SimpleGrantedAuthority("ROLE_" + role.getRoleCode()));}return authorities;}// ......省略其他屬性的set和get方法}

Mapper層實現根據手機號碼查詢用戶詳細信息代碼如下:
UserMapper.java

@Repository public interface UserMapper {UserDTO loadUserByPhoneNum(@Param("phoneNum") Long phoneNum);// ......省略其他抽象方法 }

UserMapper.xml

<select id="loadUserByPhoneNum" resultType="org.sang.pojo.dto.UserDTO">SELECT id, username, nickname,password, phoneNum, enabled, email, userface, regTime, updateTimeFROM `user`WHERE phoneNum = #{phoneNum,jdbcType=BIGINT} </select>

5 修改短信服務sendLoginVeryCodeMessage方法

關于在SpringBoot項目中如何集成騰訊云短信服務實現發送短信驗證碼功能,可以參考我之前發表在公眾號的文章SpringBoot項目中快速集成騰訊云短信SDK實現手機驗證碼功能

只是需要稍作修改,因為發短信驗證碼時要求國內手機號前綴為+86,后面接的是用戶的11位手機號碼。而我們的數據庫中存儲的是11位手機號碼,使用手機號+短信驗證碼登錄時使用的也是11位手機號碼。因此將短信驗證碼存入redis緩存時需要將這里手機號的+86前綴去掉。

如果這里不改,那么數據庫中用戶的手機號碼字段就要設計成一個字符串類型,前端用戶登錄時傳入的手機號參數也應該加上+86前綴。為了避免更多地方修改,我們就在這里修改好了。

SmsService.java

public SendSmsResponse sendLoginVeryCodeMessage(String phoneNum) {SendSmsRequest req = new SendSmsRequest();req.setSenderId(null);req.setSessionContext(null);req.setSign("阿福談Java技術棧");req.setSmsSdkAppid(smsProperty.getAppid());req.setTemplateID(SmsEnum.PHONE_CODE_LOGIN.getTemplateId());req.setPhoneNumberSet(new String[]{phoneNum});String verifyCode = getCode();String[] params = new String[]{verifyCode, "10"};req.setTemplateParamSet(params);logger.info("req={}", JSON.toJSONString(req));try {SendSmsResponse res = smsClient.SendSms(req);if ("Ok".equals(res.getSendStatusSet()[0].getCode())) {// 截掉+86字段,發送短信驗證碼成功則將驗證碼保存到redis緩存中(目前只針對國內短息業務)phoneNum = phoneNum.substring(3);redisTemplate.opsForValue().set("loginVerifyCode:"+phoneNum, verifyCode, 10, TimeUnit.MINUTES);}return res;} catch (TencentCloudSDKException e) {logger.error("send message failed", e);throw new RuntimeException("send message failed, caused by " + e.getMessage());}// 其他代碼省略

6 修改WebSecurityConfig配置類

最后我們需要修改WebSecurityConfig配置類,定義MobilePhoneAuthenticationProvider和AuthenticationManager兩個類的bean方法,同時在兩個configure方法中增加新的邏輯處理。

最后WebSecurityConfig配置類的完整代碼如下:

@Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter {@Resourceprivate UserService userService;@ResourceRedisTemplate<String, Object> redisTemplate;@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(userService);MobilePhoneAuthenticationProvider mobilePhoneAuthenticationProvider = this.mobilePhoneAuthenticationProvider();auth.authenticationProvider(mobilePhoneAuthenticationProvider);}@Overrideprotected void configure(HttpSecurity http) throws Exception {// 添加手機登錄認證過濾器,在構造函數中設置攔截認證請求路徑MobilePhoneAuthenticationFilter mobilePhoneAuthenticationFilter = new MobilePhoneAuthenticationFilter("/mobile/login");mobilePhoneAuthenticationFilter.setAuthenticationSuccessHandler(new FormLoginSuccessHandler());mobilePhoneAuthenticationFilter.setAuthenticationFailureHandler(new FormLoginFailedHandler());// 下面這個authenticationManager必須設置,否則在MobilePhoneAuthenticationFilter#attemptAuthentication// 方法中調用this.getAuthenticationManager().authenticate(authRequest)方法時會報NullPointExceptionmobilePhoneAuthenticationFilter.setAuthenticationManager(authenticationManagerBean());mobilePhoneAuthenticationFilter.setAllowSessionCreation(true);http.addFilterAfter(mobilePhoneAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);// 配置跨域http.cors().configurationSource(corsConfigurationSource());// 禁用spring security框架的退出登錄,使用自定義退出登錄http.logout().disable();http.authorizeRequests().antMatchers("/user/reg").anonymous().antMatchers("/sendLoginVerifyCode").anonymous().antMatchers("/doc.html").hasAnyRole("user", "admin").antMatchers("/admin/**").hasRole("admin")///admin/**的URL都需要有超級管理員角色,如果使用.hasAuthority()方法來配置,需要在參數中加上ROLE_,如下:hasAuthority("ROLE_超級管理員").anyRequest().authenticated()//其他的路徑都是登錄后即可訪問.and().formLogin().loginPage("http://localhost:3000/#/login").successHandler(new FormLoginSuccessHandler()).failureHandler(new FormLoginFailedHandler()).loginProcessingUrl("/user/login").usernameParameter("username").passwordParameter("password").permitAll().and().logout().permitAll().and().csrf().disable().exceptionHandling().accessDeniedHandler(getAccessDeniedHandler());}@Overridepublic void configure(WebSecurity web) throws Exception {web.ignoring().antMatchers("/blogimg/**","/index.html","/static/**");}@BeanAccessDeniedHandler getAccessDeniedHandler() {return new AuthenticationAccessDeniedHandler();}//配置跨域訪問資源private CorsConfigurationSource corsConfigurationSource() {UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();CorsConfiguration corsConfiguration = new CorsConfiguration();corsConfiguration.addAllowedOrigin("*"); //同源配置,*表示任何請求都視為同源,若需指定ip和端口可以改為如“localhost:8080”,多個以“,”分隔;corsConfiguration.addAllowedHeader("*");//header,允許哪些header,本案中使用的是token,此處可將*替換為token;corsConfiguration.addAllowedMethod("*"); //允許的請求方法,PSOT、GET等corsConfiguration.setAllowCredentials(true);// 注冊跨域配置source.registerCorsConfiguration("/**",corsConfiguration); //配置允許跨域訪問的urlreturn source;}@Override@Beanpublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}@Beanpublic MobilePhoneAuthenticationProvider mobilePhoneAuthenticationProvider() {MobilePhoneAuthenticationProvider mobilePhoneAuthenticationProvider = new MobilePhoneAuthenticationProvider(userService, redisTemplate);return mobilePhoneAuthenticationProvider;} }

7 驗證效果

編碼完成后,我們在啟動Mysql服務器和Redis服務器后啟動我們的SpringBoot項目

首先在Postman中調用發送短信驗證碼接口


驗證碼發送成功后返回如下響應信息:

{"status": 200,"msg": "success","data": {"code": "Ok","phoneNumber": "+8618682244076","fee": 1,"message": "send success"} }

同時手機上也會受到6位短信驗證碼,有效期10分鐘

然后我們使用自己的手機號+收到的6位短信驗證碼調用登錄接口


登錄成功后返回如下響應信息:

{"msg": "login success","userInfo": {"accountNonExpired": true,"accountNonLocked": true,"authorities": [{"authority": "ROLE_admin"},{"authority": "ROLE_user"},{"authority": "ROLE_test1"}],"credentialsNonExpired": true,"currentRole": {"id": 1,"roleCode": "admin","roleName": "管理員"},"email": "heshengfu2018@163.com","enabled": true,"id": 3,"nickname": "程序員阿福","phoneNum": 18682244076,"regTime": 1624204813000,"roles": [{"$ref": "$.userInfo.currentRole"},{"id": 2,"roleCode": "user","roleName": "普通用戶"},{"id": 3,"roleCode": "test1","roleName": "測試角色1"}],"username": "heshengfu"},"status": "success" }

到這里,實現在集成SpringSecurity的SpringBoot應用中增加手機號+短信碼的方式登錄認證的功能也就實現了。各位讀者朋友如果覺得文章對你有幫助,歡迎給我的這篇文章點個在看并轉發給身邊的程序員同事和朋友,謝謝!后面有時間筆者會在前端用戶登錄界面調用本次實現的后臺接口實現手機號+短信驗證碼功能。

以下是這邊文章在本人的gitee倉庫的源碼地址,需要研究的完整代碼的朋友可以克隆到自己本地。

blogserver項目gitee克隆地址: https://gitee.com/heshengfu1211/blogserver.git

本文首發個人微信公眾號【阿福談Web編程】,覺得我的文章對你有幫助或者有什么疑問需要與我進行交流的讀者朋友歡迎關注我的微信公眾號。關注后可在我的微信公眾號菜單欄里點擊【聯系作者】,就會發送筆者的微信二維碼給你。筆者期待在技術精進的路上遇到越來越多同行的盆友,讓我們在IT技術學習的路上不孤單!

總結

以上是生活随笔為你收集整理的手把手带你在集成SpringSecurity的SpringBoot应用中添加短信验证码登录认证功能的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。