springsecurity實現(xiàn)用戶登錄認(rèn)證快速使用示例代碼(前后端分離項目)
ps:該文章適合未系統(tǒng)學(xué)習(xí)springsecurity快速使用,可以直接cv使用,只有部分源碼講解,個人覺得先會用了再深究原理
1、引入依賴
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> <version>2.6.13</version> </dependency>
引入springsecurity依賴后,該依賴會自動生成默認(rèn)登陸頁面和登錄名(user)和密碼(控制臺)
我們使用這個用戶名和密碼登陸后才可以對資源進(jìn)行訪問
因為項目是前后端分離項目,因此并不需要它默認(rèn)生成的登錄頁面和默認(rèn)用戶名密碼嗎,需要查詢數(shù)據(jù)庫進(jìn)行登錄
2、創(chuàng)建類繼承WebSecurityConfigurerAdapter
我們創(chuàng)建一個配置類SecurityConfig(使用@Configuration標(biāo)記)并且繼承WebSecurityConfigurerAdapter類,重寫里面的幾個配置方法即可對springsecurity進(jìn)行配置
(1)重寫里面的configure(HttpSecurity http)方法
跟進(jìn)configure(HttpSecurity http)查看源碼中的方法做了什么
我們可以看到源碼中的該方法中默認(rèn)對所有的請求進(jìn)行攔截,并且默認(rèn)生成表單登錄頁面,并且使用基本認(rèn)證。
我們只需要重寫該方法就可以自己進(jìn)行配置了
.csrf().disable() .cors() csrf建議關(guān)閉,cors前后端分離項目建議打開
.mvcMatchers("/admin/login").anonymous() 對這個接口可以匿名訪問,也就是不需要認(rèn)證
.mvcMatchers("/admin/save").permitAll() 對這個接口也不做認(rèn)證
.mvcMatchers("/user/save").authenticated() 對這個接口需要認(rèn)證才能訪問
這個.mvcMatchers的參數(shù)也可以是數(shù)組形式
.addFilterBefore(tokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
添加過濾器,這里的tokenAuthenticationFilter是我自己定義的,這個意思將自定義的這個過濾器放在UsernamePasswordAuthenticationFilter.class這個過濾器之前,這個過濾器是springsecurity提供的認(rèn)證過濾器,像我們的這個tokenAuthenticationFilter是需要在認(rèn)證之前進(jìn)行的
.exceptionHandling() .authenticationEntryPoint(authenticationEntryPoint);
這個是添加了一個認(rèn)證異常處理器authenticationEntryPoint,authenticationEntryPoint也是我們自定義的,就是當(dāng)用戶未經(jīng)過認(rèn)證時返回的結(jié)果,通常當(dāng)未登錄訪問接口時返回給前端的異常信息就在這里定義
這樣我們就大致完成了這個方法中登錄認(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(); }
使用這個進(jìn)行密碼加密,springsecurity提供了很多密碼加密方法,用這個就可以,可以點進(jìn)去查看PasswordEncoder這個方法,這是個接口,實現(xiàn)了很多加密方法
然后這樣我們就大致完成了這個類的配置
如果有swagger等靜態(tài)資源配置,可以重寫這個方法
/** * 配置哪些請求不攔截 * 排除swagger相關(guān)請求 * @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(); } /** * 配置哪些請求不攔截 * 排除swagger相關(guān)請求 * @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"); } }
防止報錯還有這個自定義的登錄攔截器跟認(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","訪問失敗,請重新登錄"); 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","訪問失敗,該資源受到保護(hù)..."); response.getWriter().print(errMsg.toString()); } }
等會再說這兩個配置
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; } }
我們得重寫里面的幾個方法,并且把我們的用戶類給整進(jìn)來
Collection<? extends GrantedAuthority> getAuthorities()
這個是權(quán)限,我們返回null就行了,這會登錄用不到
public String getPassword() {return admin.getPassword();}
這個方法是獲取密碼,也就是security會從這里獲取登錄的密碼,我們就把我們的用戶類的密碼讓他返回
public String getUsername() { return admin.getUsername();}
這個是獲取用戶名的方法,也就是security會從這里獲取登錄的用戶名,我們就把我們的用戶類的用戶名讓他返回
@Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; }
這幾個都是賬號相關(guān)的,什么賬號是否被鎖定、是否啟用在這里返回結(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類型的
這就是為啥在配置的時候?qū)?/p>
AuthenticationManager authenticationManagerBean()這個使用@Bean標(biāo)記
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginDto.getUsername(),loginDto.getPassword());
這個類就可以將我們的用戶名和密碼封裝成繼承了Authentication類型的類然后用于登錄
UsernamePasswordAuthenticationToken()他的參數(shù)是
Object principal, Object credentials
這就分別是用戶名和登陸憑證也就是密碼
然后登陸成功后返回一個Authentication類型,然后.getPrincipal()這個方法就可以獲取登錄的用戶信息。
5、是怎么完成登錄的
這時候我們來看登錄流程圖(圖是盜的)
當(dāng)我們調(diào)用Authentication authenticate = authenticationManager.authenticate(authenticationToken);這個方法的時候做了什么?
我們關(guān)注兩步就可以了
第一個就是根據(jù)用戶名查詢用戶,第二個就是進(jìn)行密碼比對
(1)根據(jù)用戶名查詢用戶
因為在執(zhí)行認(rèn)證的方法后,會調(diào)用DaoAuthencationProvider中的UserDetailService對象中的loadUserByUsername這個方法,如果基于springsecurity的默認(rèn)配置,這個方法就是實現(xiàn)了UserDetailService這個接口的InMemoryUserDetailsManager這個方法中的loadUserByUsername
我們可以看到進(jìn)行登錄時候調(diào)用了loadUserByUsername的方法,這個方法是在
UserDetailsService中寫的,看方法名也知道是根據(jù)用戶名查找用戶
因為我們要查的是數(shù)據(jù)庫中的用戶數(shù)據(jù),我們就可以也可以實現(xiàn)UserDetailService并且重寫里面的loadUserByUsername方法 根據(jù)數(shù)據(jù)庫查詢出用戶信息并返回繼承了UserDetail的AdminLogin 類
@Service public class AdminDetailsServiceImpl implements UserDetailsService { @Autowired private AdminService adminService; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { //根據(jù)用戶名查詢數(shù)據(jù)庫中的用戶 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)行下一步密碼對比,如果用戶不存在就拋異常
(2)密碼對比
這個springsecurity都已經(jīng)寫好了,我們看看源碼就行,找到那個我們自定義的security的配置類
ctrl點進(jìn)BCryptPasswordEncoder加密方式
里面的boolean matches(CharSequence rawPassword, String encodedPassword)這個方法就是密碼對比,它里面又會執(zhí)行BCrypt.checkpw(rawPassword.toString(), encodedPassword)這個方法。總之就是將前端傳來的密碼進(jìn)行加密后與數(shù)據(jù)庫的進(jìn)行對比
為啥不能將數(shù)據(jù)庫的密碼解析后對比傳來的明文密碼呢?因為他這個加密之后是不可逆的
然后到這登錄基本就完事了
新問題,數(shù)據(jù)庫中還沒加密后的用戶數(shù)據(jù)怎么辦?
6、注冊用戶加密密碼
將前端傳來的密碼使用security配置類中的加密方式加密后就行
7、登錄過濾器
在前面配置的時候已經(jīng)整過代碼了,在這里獲取token并校驗,校驗完之后獲取里面的用戶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","訪問失敗,請重新登錄"); 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)證時訪問資源提示的信息
@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","訪問失敗,該資源受到保護(hù)..."); response.getWriter().print(errMsg.toString()); } }
到此這篇關(guān)于springsecurity實現(xiàn)用戶登錄認(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)全過程
- Spring Security實現(xiàn)登錄認(rèn)證實戰(zhàn)教程
- SpringSecurity 自定義認(rèn)證登錄的項目實踐
- spring security登錄認(rèn)證授權(quán)的項目實踐
相關(guān)文章
如何通過RabbitMq實現(xiàn)動態(tài)定時任務(wù)詳解
工作中經(jīng)常會有定時任務(wù)的需求,常見的做法可以使用Timer、Quartz、Hangfire等組件,這次想嘗試下新的思路,使用RabbitMQ死信隊列的機(jī)制來實現(xiàn)定時任務(wù),下面這篇文章主要給大家介紹了關(guān)于如何通過RabbitMq實現(xiàn)動態(tài)定時任務(wù)的相關(guān)資料,需要的朋友可以參考下2022-01-01