springsecurity實(shí)現(xiàn)用戶登錄認(rèn)證快速使用示例代碼(前后端分離項(xiàng)目)
ps:該文章適合未系統(tǒng)學(xué)習(xí)springsecurity快速使用,可以直接cv使用,只有部分源碼講解,個(gè)人覺得先會(huì)用了再深究原理
1、引入依賴
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> <version>2.6.13</version> </dependency>
引入springsecurity依賴后,該依賴會(huì)自動(dòng)生成默認(rèn)登陸頁(yè)面和登錄名(user)和密碼(控制臺(tái))
我們使用這個(gè)用戶名和密碼登陸后才可以對(duì)資源進(jìn)行訪問(wèn)
因?yàn)轫?xiàng)目是前后端分離項(xiàng)目,因此并不需要它默認(rèn)生成的登錄頁(yè)面和默認(rèn)用戶名密碼嗎,需要查詢數(shù)據(jù)庫(kù)進(jìn)行登錄
2、創(chuàng)建類繼承WebSecurityConfigurerAdapter
我們創(chuàng)建一個(gè)配置類SecurityConfig(使用@Configuration標(biāo)記)并且繼承WebSecurityConfigurerAdapter類,重寫里面的幾個(gè)配置方法即可對(duì)springsecurity進(jìn)行配置
(1)重寫里面的configure(HttpSecurity http)方法
跟進(jìn)configure(HttpSecurity http)查看源碼中的方法做了什么
我們可以看到源碼中的該方法中默認(rèn)對(duì)所有的請(qǐng)求進(jìn)行攔截,并且默認(rèn)生成表單登錄頁(yè)面,并且使用基本認(rèn)證。
我們只需要重寫該方法就可以自己進(jìn)行配置了
.csrf().disable() .cors() csrf建議關(guān)閉,cors前后端分離項(xiàng)目建議打開
.mvcMatchers("/admin/login").anonymous() 對(duì)這個(gè)接口可以匿名訪問(wèn),也就是不需要認(rèn)證
.mvcMatchers("/admin/save").permitAll() 對(duì)這個(gè)接口也不做認(rèn)證
.mvcMatchers("/user/save").authenticated() 對(duì)這個(gè)接口需要認(rèn)證才能訪問(wèn)
這個(gè).mvcMatchers的參數(shù)也可以是數(shù)組形式
.addFilterBefore(tokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
添加過(guò)濾器,這里的tokenAuthenticationFilter是我自己定義的,這個(gè)意思將自定義的這個(gè)過(guò)濾器放在UsernamePasswordAuthenticationFilter.class這個(gè)過(guò)濾器之前,這個(gè)過(guò)濾器是springsecurity提供的認(rèn)證過(guò)濾器,像我們的這個(gè)tokenAuthenticationFilter是需要在認(rèn)證之前進(jìn)行的
.exceptionHandling() .authenticationEntryPoint(authenticationEntryPoint);
這個(gè)是添加了一個(gè)認(rèn)證異常處理器authenticationEntryPoint,authenticationEntryPoint也是我們自定義的,就是當(dāng)用戶未經(jīng)過(guò)認(rèn)證時(shí)返回的結(jié)果,通常當(dāng)未登錄訪問(wèn)接口時(shí)返回給前端的異常信息就在這里定義
這樣我們就大致完成了這個(gè)方法中登錄認(rèn)證功能的一些配置
(2)重寫AuthenticationManager authenticationManagerBean()
我們需要使用他里面的方法進(jìn)行登錄認(rèn)證,并使用@Bean標(biāo)注到spring容器中
@Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); }
(3)密碼加密工具
@Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); }
使用這個(gè)進(jìn)行密碼加密,springsecurity提供了很多密碼加密方法,用這個(gè)就可以,可以點(diǎn)進(jìn)去查看PasswordEncoder這個(gè)方法,這是個(gè)接口,實(shí)現(xiàn)了很多加密方法
然后這樣我們就大致完成了這個(gè)類的配置
如果有swagger等靜態(tài)資源配置,可以重寫這個(gè)方法
/** * 配置哪些請(qǐng)求不攔截 * 排除swagger相關(guān)請(qǐng)求 * @param web * @throws Exception */ @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/favicon.ico","/swagger-resources/**", "/webjars/**", "/v2/**", "/swagger-ui.html/**", "/doc.html"); }
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private TokenAuthenticationFilter tokenAuthenticationFilter; @Autowired private AuthenticationEntryPointImpl authenticationEntryPoint; /* @Autowired private UserDetailsService userDetailsService;*/ @Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Override protected void configure(HttpSecurity http) throws Exception { http .csrf().disable() .cors() .and() .authorizeRequests() .mvcMatchers("/admin/login").anonymous() .mvcMatchers("/admin/save").permitAll() .mvcMatchers("/wx/user/login").permitAll() .mvcMatchers("/wx/user/save").permitAll() .anyRequest().authenticated() .and() .addFilterBefore(tokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) .exceptionHandling() .authenticationEntryPoint(authenticationEntryPoint); } @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } /** * 配置哪些請(qǐng)求不攔截 * 排除swagger相關(guān)請(qǐng)求 * @param web * @throws Exception */ @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/favicon.ico","/swagger-resources/**", "/webjars/**", "/v2/**", "/swagger-ui.html/**", "/doc.html"); } }
防止報(bào)錯(cuò)還有這個(gè)自定義的登錄攔截器跟認(rèn)證失敗處理器也整上
@Component public class TokenAuthenticationFilter extends OncePerRequestFilter { @Autowired private StringRedisTemplate redisTemplate; @Override protected void doFilterInternal (HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String token = request.getHeader("token"); if (ObjectUtils.isEmpty(token)){ filterChain.doFilter(request,response); return; } Claims claims = null; try { claims = JwtUtil.parseJWT(token); } catch (Exception e) { e.printStackTrace(); Map<String, String> errMsg = new HashMap<>(); errMsg.put("code","200"); errMsg.put("msg","訪問(wèn)失敗,請(qǐng)重新登錄"); response.setContentType("text/json;charset=utf-8"); response.getWriter().print(errMsg.toString()); return; } Integer userId = Integer.valueOf(claims.getSubject()); UserContext.setUser(userId); String userAdmin = redisTemplate.opsForValue().get("userId" + userId); AdminLogin adminLogin = JSONUtil.toBean(userAdmin, AdminLogin.class); UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(adminLogin.getUsername(), adminLogin.getUsername(), null); // UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(null, null, null); SecurityContextHolder.getContext().setAuthentication(authenticationToken); filterChain.doFilter(request,response); } }
@Component public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint, Serializable { private static final long serialVersionUID = -8970718410437077606L; @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException { Map<String, String> errMsg = new HashMap<>(); response.setContentType("text/json;charset=utf-8"); errMsg.put("code","200"); errMsg.put("msg","訪問(wèn)失敗,該資源受到保護(hù)..."); response.getWriter().print(errMsg.toString()); } }
等會(huì)再說(shuō)這兩個(gè)配置
3、繼承UserDetails
用我們的登錄的用戶類繼承UserDetails,我這里是Admin
@Data @AllArgsConstructor @NoArgsConstructor public class AdminLogin implements UserDetails { private Admin admin; @Override public Collection<? extends GrantedAuthority> getAuthorities() { return null; } @Override public String getPassword() { return admin.getPassword(); } @Override public String getUsername() { return admin.getUsername(); } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } }
我們得重寫里面的幾個(gè)方法,并且把我們的用戶類給整進(jìn)來(lái)
Collection<? extends GrantedAuthority> getAuthorities()
這個(gè)是權(quán)限,我們返回null就行了,這會(huì)登錄用不到
public String getPassword() {return admin.getPassword();}
這個(gè)方法是獲取密碼,也就是security會(huì)從這里獲取登錄的密碼,我們就把我們的用戶類的密碼讓他返回
public String getUsername() { return admin.getUsername();}
這個(gè)是獲取用戶名的方法,也就是security會(huì)從這里獲取登錄的用戶名,我們就把我們的用戶類的用戶名讓他返回
@Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; }
這幾個(gè)都是賬號(hào)相關(guān)的,什么賬號(hào)是否被鎖定、是否啟用在這里返回結(jié)果,我們返回true,如果返回false就登錄不了了
4、登錄方法
@Service public class AdminLoginServiceImpl implements AdminLoginService { @Autowired private AuthenticationManager authenticationManager; //管理員登錄 @Override public Result adminLogin(LoginDto loginDto) { UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginDto.getUsername(), loginDto.getPassword()); Authentication authenticate = authenticationManager.authenticate(authenticationToken); AdminLogin adminLogin = (AdminLogin) authenticate.getPrincipal(); String jwt = JwtUtil.createJWT(String.valueOf(adminLogin.getAdmin().getId())); //用戶信息 redisTemplate.opsForValue().set("userId"+adminLogin.getAdmin().getId(), JSONUtil.toJsonStr(adminLogin)); return Result.success(jwt); } }
登錄的方法就是調(diào)用
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
它里面需要接受的參數(shù)類型必須是Authentication類型的
這就是為啥在配置的時(shí)候?qū)?/p>
AuthenticationManager authenticationManagerBean()這個(gè)使用@Bean標(biāo)記
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginDto.getUsername(),loginDto.getPassword());
這個(gè)類就可以將我們的用戶名和密碼封裝成繼承了Authentication類型的類然后用于登錄
UsernamePasswordAuthenticationToken()他的參數(shù)是
Object principal, Object credentials
這就分別是用戶名和登陸憑證也就是密碼
然后登陸成功后返回一個(gè)Authentication類型,然后.getPrincipal()這個(gè)方法就可以獲取登錄的用戶信息。
5、是怎么完成登錄的
這時(shí)候我們來(lái)看登錄流程圖(圖是盜的)
當(dāng)我們調(diào)用Authentication authenticate = authenticationManager.authenticate(authenticationToken);這個(gè)方法的時(shí)候做了什么?
我們關(guān)注兩步就可以了
第一個(gè)就是根據(jù)用戶名查詢用戶,第二個(gè)就是進(jìn)行密碼比對(duì)
(1)根據(jù)用戶名查詢用戶
因?yàn)樵趫?zhí)行認(rèn)證的方法后,會(huì)調(diào)用DaoAuthencationProvider中的UserDetailService對(duì)象中的loadUserByUsername這個(gè)方法,如果基于springsecurity的默認(rèn)配置,這個(gè)方法就是實(shí)現(xiàn)了UserDetailService這個(gè)接口的InMemoryUserDetailsManager這個(gè)方法中的loadUserByUsername
我們可以看到進(jìn)行登錄時(shí)候調(diào)用了loadUserByUsername的方法,這個(gè)方法是在
UserDetailsService中寫的,看方法名也知道是根據(jù)用戶名查找用戶
因?yàn)槲覀円榈氖菙?shù)據(jù)庫(kù)中的用戶數(shù)據(jù),我們就可以也可以實(shí)現(xiàn)UserDetailService并且重寫里面的loadUserByUsername方法 根據(jù)數(shù)據(jù)庫(kù)查詢出用戶信息并返回繼承了UserDetail的AdminLogin 類
@Service public class AdminDetailsServiceImpl implements UserDetailsService { @Autowired private AdminService adminService; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { //根據(jù)用戶名查詢數(shù)據(jù)庫(kù)中的用戶 LambdaQueryWrapper<Admin> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(Admin::getUsername,username); Admin admin = adminService.getOne(wrapper); //如果根據(jù)用戶名查找不到用戶 if (ObjectUtils.isEmpty(admin)){ throw new TxdException(208,"用戶不存在"); } //返回adminLogin AdminLogin adminLogin = new AdminLogin(admin); return adminLogin; } }
如果能查到用戶就返回,然后進(jìn)行下一步密碼對(duì)比,如果用戶不存在就拋異常
(2)密碼對(duì)比
這個(gè)springsecurity都已經(jīng)寫好了,我們看看源碼就行,找到那個(gè)我們自定義的security的配置類
ctrl點(diǎn)進(jìn)BCryptPasswordEncoder加密方式
里面的boolean matches(CharSequence rawPassword, String encodedPassword)這個(gè)方法就是密碼對(duì)比,它里面又會(huì)執(zhí)行BCrypt.checkpw(rawPassword.toString(), encodedPassword)這個(gè)方法??傊褪菍⑶岸藗鱽?lái)的密碼進(jìn)行加密后與數(shù)據(jù)庫(kù)的進(jìn)行對(duì)比
為啥不能將數(shù)據(jù)庫(kù)的密碼解析后對(duì)比傳來(lái)的明文密碼呢?因?yàn)樗@個(gè)加密之后是不可逆的
然后到這登錄基本就完事了
新問(wèn)題,數(shù)據(jù)庫(kù)中還沒加密后的用戶數(shù)據(jù)怎么辦?
6、注冊(cè)用戶加密密碼
將前端傳來(lái)的密碼使用security配置類中的加密方式加密后就行
7、登錄過(guò)濾器
在前面配置的時(shí)候已經(jīng)整過(guò)代碼了,在這里獲取token并校驗(yàn),校驗(yàn)完之后獲取里面的用戶id,根據(jù)用戶id獲取redis里面的數(shù)據(jù),并將用戶信息使用UsernamePasswordAuthenticationToken 封裝并且放入SecurityContextHolder.getContext().setAuthentication(authenticationToken);中
@Component public class TokenAuthenticationFilter extends OncePerRequestFilter { @Autowired private StringRedisTemplate redisTemplate; @Override protected void doFilterInternal (HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String token = request.getHeader("token"); if (ObjectUtils.isEmpty(token)){ filterChain.doFilter(request,response); return; } Claims claims = null; try { claims = JwtUtil.parseJWT(token); } catch (Exception e) { e.printStackTrace(); Map<String, String> errMsg = new HashMap<>(); errMsg.put("code","200"); errMsg.put("msg","訪問(wèn)失敗,請(qǐng)重新登錄"); response.setContentType("text/json;charset=utf-8"); response.getWriter().print(errMsg.toString()); return; } Integer userId = Integer.valueOf(claims.getSubject()); UserContext.setUser(userId); String userAdmin = redisTemplate.opsForValue().get("userId" + userId); AdminLogin adminLogin = JSONUtil.toBean(userAdmin, AdminLogin.class); UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(adminLogin.getUsername(), adminLogin.getUsername(), null); // UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(null, null, null); SecurityContextHolder.getContext().setAuthentication(authenticationToken); filterChain.doFilter(request,response); } }
8、認(rèn)證失敗處理器
當(dāng)用戶未認(rèn)證時(shí)訪問(wèn)資源提示的信息
@Component public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint, Serializable { private static final long serialVersionUID = -8970718410437077606L; @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException { Map<String, String> errMsg = new HashMap<>(); response.setContentType("text/json;charset=utf-8"); errMsg.put("code","200"); errMsg.put("msg","訪問(wèn)失敗,該資源受到保護(hù)..."); response.getWriter().print(errMsg.toString()); } }
到此這篇關(guān)于springsecurity實(shí)現(xiàn)用戶登錄認(rèn)證快速使用(前后端分離項(xiàng)目)的文章就介紹到這了,更多相關(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ò)程
- Spring Security實(shí)現(xiàn)登錄認(rèn)證實(shí)戰(zhàn)教程
- SpringSecurity 自定義認(rèn)證登錄的項(xiàng)目實(shí)踐
- spring security登錄認(rèn)證授權(quán)的項(xiàng)目實(shí)踐
相關(guān)文章
Spring Bean 依賴注入常見錯(cuò)誤問(wèn)題
這篇文章主要介紹了Spring Bean 依賴注入常見錯(cuò)誤問(wèn)題,文中提到value的工作大體分為三個(gè)核心步驟,具體內(nèi)容詳情跟隨小編一起看看吧2021-09-09如何通過(guò)RabbitMq實(shí)現(xiàn)動(dòng)態(tài)定時(shí)任務(wù)詳解
工作中經(jīng)常會(huì)有定時(shí)任務(wù)的需求,常見的做法可以使用Timer、Quartz、Hangfire等組件,這次想嘗試下新的思路,使用RabbitMQ死信隊(duì)列的機(jī)制來(lái)實(shí)現(xiàn)定時(shí)任務(wù),下面這篇文章主要給大家介紹了關(guān)于如何通過(guò)RabbitMq實(shí)現(xiàn)動(dòng)態(tài)定時(shí)任務(wù)的相關(guān)資料,需要的朋友可以參考下2022-01-01使用SpringBoot設(shè)置虛擬路徑映射絕對(duì)路徑
這篇文章主要介紹了使用SpringBoot設(shè)置虛擬路徑映射絕對(duì)路徑的操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-08-08