springSecurity使用實戰(zhàn)指南
一.登錄security
1.自定義用戶賬號和密碼
step01-先導入兩個依賴


方式一:在核心配置文件下設(shè)定信息(不合理)
spring.security.user.password=123 spring.security.user.name=pan
方式二:根據(jù)數(shù)據(jù)庫數(shù)據(jù)進行登錄驗證(UserDetails)(重要)
step01-mysql的連接
spring.datasource.username=root spring.datasource.password=123456 spring.datasource.url=jdbc:mysql:///test02?serverTimezone=Asia/Shanghai
step02-實體類實現(xiàn)UserDetails(是SpringSecurity中定義的用戶定義規(guī)范)
//獲取當前用戶的權(quán)限/角色
Collection<? extends GrantedAuthority> getAuthorities();
//獲取用戶密碼
String getPassword();
//如果SpringSecurity框架想要獲取用戶名就會調(diào)用改方法,返回的變量名叫什么都行
String getUsername();
//判斷當前用戶賬戶是否沒有過期,正常來說在數(shù)據(jù)庫中,有一個字段可以顯示該賬戶狀態(tài)
boolean isAccountNonExpired();
//當前用戶是否沒有鎖定
boolean isAccountNonLocked();
//賬戶密碼是否沒有過期
boolean isCredentialsNonExpired();
//當前用戶是否可用,正常來說在數(shù)據(jù)庫中,有一個字段可以顯示該賬戶狀態(tài)
boolean isEnabled();public class User implements UserDetails {
private Integer id;
private String username;
private String address;
private String password;
private String favorites;
private Integer gender;
private String grade;
private Integer age;
private boolean enabled;
/**
* 獲取當前用戶的角色
* @return
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
/**
* 獲取用戶密碼
* @return
*/
@Override
public String getPassword() {
return password;
}
/**
* 如果 Spring Security 框架需要獲取到用戶名,他就會調(diào)用這個方法
*
* 所以,該方法返回值的變量名稱,實際上無所謂,變成名稱可以是任何名字
*
* @return
*/
@Override
public String getUsername() {
return username;
}
/**
* 判斷當前賬戶是否過期,正常來說,應(yīng)該在數(shù)據(jù)庫中,有一個賬戶是否過期的字段,在這里直接返回即可
* @return
*/
@Override
public boolean isAccountNonExpired() {
return true;
}
/**
* 賬戶是否沒有鎖定
* @return
*/
@Override
public boolean isAccountNonLocked() {
return true;
}
/**
* 賬戶密碼是否沒有過期
* @return
*/
@Override
public boolean isCredentialsNonExpired() {
return true;
}
/**
* 賬戶是否可用
* @return
*/
@Override
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public void setUsername(String username) {
this.username = username;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public void setPassword(String password) {
this.password = password;
}
public String getFavorites() {
return favorites;
}
public void setFavorites(String favorites) {
this.favorites = favorites;
}
public Integer getGender() {
return gender;
}
public void setGender(Integer gender) {
this.gender = gender;
}
public String getGrade() {
return grade;
}
public void setGrade(String grade) {
this.grade = grade;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", username=" + username +
", address=" + address +
", password=" + password +
", favorites=" + favorites +
", gender=" + gender +
", grade=" + grade +
", age=" + age +
"}";
}
}第二步:在實現(xiàn)service層實現(xiàn)UserDetailsService,在loadUserByUsername中判斷前端與數(shù)據(jù)庫中的值是否一致,和shiro的Realm相似
@Service
public class UserService implements UserDetailsService {
@Autowired
UserMapper userMapper;
/**
* 系統(tǒng)登錄的時候,該方法會被自動調(diào)用
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userMapper.loadUserByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("賬戶不存在");
}
//查詢用戶角色
user.setRoles(userMapper.getRolesByUid(user.getId()));
//返回給User類中,因為該類實現(xiàn)UserDetails的方法中的很多方法,這些方法是將值給SpringSecurity
return user;
}
}2.自制登錄頁面(SecurityFilterChain)
第一步:需要設(shè)置一個登錄界面,還有在Controller層輸寫一個登錄界面的接口
第二步:配置類,改登錄界面的路徑
注意:security底層就是使用過濾器,一共有16個過濾器,稱為過濾器鏈;
1.spring5以前,springboot2.7.7
@Configuration
public class SecurityConfig {
/**
*spring5之前配置spring security的過濾器鏈
*/
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
//開始過濾器的配置
.authorizeRequests()
//攔截所有請求,這個就相當于 /**
.anyRequest()
//表示攔截下來的請求,都必須認證(登錄)之后才能訪問
.authenticated()
.and()
//開始登錄表單的配置,其實配置的是 UsernamePasswordAuthenticationFilter 過濾器
.formLogin()
//首先指定登錄頁面的地址
//登錄頁面,默認是 /login GET 請求
.loginPage("/login")
//指定登錄接口的地址
//默認情況下,登錄接口也是 /login POST 請求
.loginProcessingUrl("/doLogin")
//配置登錄的用戶名的 key,默認就是 username
.usernameParameter("username")
//配置登錄的用戶密碼的 key,默認就是 password
.passwordParameter("password")
//登錄成功之后的跳轉(zhuǎn)頁面,這個是服務(wù)端跳轉(zhuǎn),請求轉(zhuǎn)發(fā)
// .successForwardUrl("/hello")
//登錄成功之后的跳轉(zhuǎn)頁面,這個是客戶端跳轉(zhuǎn),重定向
//假設(shè)一開始訪問 /a,系統(tǒng)自動跳轉(zhuǎn)到登錄頁面,登錄成功之后,就會跳轉(zhuǎn)回 /a,就是你想訪問哪個網(wǎng)頁的,等你登錄后,直接訪問該網(wǎng)頁
//假設(shè)一開始就訪問登錄頁面,那么登錄成功之后,才會來到 /hello
.defaultSuccessUrl("/hello")
//允許登錄表單的路徑訪問得到
.permitAll()
.and()
//禁用 csrf 保存策略,本質(zhì)上就是從 Security 過濾器鏈中移除 CsrfFilter 過濾器
.csrf()
.disable();
return http.build();
//第一個參數(shù)是攔截路徑,第二個參數(shù)是寫過濾器,這里沒有寫過濾器
//這個配置表示攔截所有的請求,但是攔截下來的之后,不經(jīng)過任意過濾器
// return new DefaultSecurityFilterChain(new AntPathRequestMatcher("/**"), new ArrayList<>());
}
}2.spring6之后,springboot3.0.1
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
//這個配置表示攔截所有請求,但是攔截下來之后,不經(jīng)過任何過濾器
// return new DefaultSecurityFilterChain(new AntPathRequestMatcher("/**"));
http.authorizeHttpRequests()
// /login.css 這個地址,不需要登錄就可以訪問
// .requestMatchers("/login.css").permitAll()
.requestMatchers("/hello").permitAll()
//攔截所有請求
.requestMatchers("/**")
//所有請求都必須登錄之后才可以訪問
.authenticated()
.and()
//表示禁用 csrf 防御機制,這個禁用的本質(zhì)就是將 CsrfFilter 從過濾器鏈中移除掉
.csrf().disable()
//開啟表單登錄,如果沒有自己去配置 SecurityFilterChain,默認表單登錄是開啟的,但是如果自己配置了,則默認的表單登錄就會被覆蓋掉,所以要重新配置
.formLogin()
//配置登錄接口,登錄接口是 POST 請求,這個地方,如果不配置,默認地址是 /login
.loginProcessingUrl("/doLogin")
//配置登錄用戶名的 key,默認就是 username
.usernameParameter("name")
//配置登陸密碼的 key,默認是 password
.passwordParameter("passwd")
//配置登錄頁面,默認登錄頁面地址是 /login(GET 請求)
.loginPage("/login.html")
//登錄成功之后的跳轉(zhuǎn)頁面,這個跳轉(zhuǎn)方式是請求轉(zhuǎn)發(fā)(服務(wù)端跳轉(zhuǎn))
//一般不用這種
// .successForwardUrl()
//默認登錄成功之后的跳轉(zhuǎn)頁面,這個是重定向跳轉(zhuǎn)
//如果用戶首先訪問 /a,但是因為沒有登錄,自動跳轉(zhuǎn)到登錄頁面,登錄成功之后,就會跳轉(zhuǎn)回 /a
//如果用戶一開始直接就訪問的是登錄頁面,那么登錄成功之后,就會跳轉(zhuǎn)到 /hello
// .defaultSuccessUrl("/hello")
//登錄成功的處理器
//auth 表示當前登錄成功的用戶對象
.successHandler((req, resp, auth) -> {
//獲取當前登錄成功的用戶對象
User user = (User) auth.getPrincipal();
user.setPassword(null);
resp.setContentType("application/json;charset=utf-8");
Map<String, Object> map = new HashMap<>();
map.put("status", 200);
map.put("message", "登錄成功");
map.put("data", user);
resp.getWriter().write(new ObjectMapper().writeValueAsString(map));
})
//失敗之后的跳轉(zhuǎn)路徑(請求轉(zhuǎn)發(fā))
// .failureForwardUrl()
//失敗路徑
// .failureUrl()
//配置失敗的處理器
.failureHandler((req, resp, e) -> {
resp.setContentType("application/json;charset=utf-8");
Map<String, Object> map = new HashMap<>();
map.put("status", 500);
map.put("message", "登錄失敗");
if (e instanceof BadCredentialsException) {
map.put("message", "用戶名或者密碼輸入錯誤,登錄失敗");
} else if (e instanceof AccountExpiredException) {
map.put("message", "賬戶過期,登錄失敗");
} else if (e instanceof CredentialsExpiredException) {
map.put("message", "密碼過期,登錄失敗");
} else if (e instanceof DisabledException) {
map.put("message", "賬戶被禁用,登錄失敗");
} else if (e instanceof LockedException) {
map.put("message", "賬戶被鎖定,登錄失敗");
}
resp.getWriter().write(new ObjectMapper().writeValueAsString(map));
})
.permitAll();
return http.build();
}3.放行策略
方法一:配置過濾器鏈(一般使用接口放行使用)
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
//開始過濾器的配置
.authorizeRequests()
//允許/css/login.css匿名訪問,就是給靜態(tài)資源直接訪問,不用攔截的,不能在anyrequest之后執(zhí)行
.antMatchers("/css/login.css")
//允許所有用戶
.permitAll()
//允許匿名用戶訪問,不允許登錄用戶訪問,所以我們正常使用permitall
//.anonymous()
//攔截所有請求,這個就相當于 /**
.anyRequest()
//表示攔截下來的請求,都必須認證(登錄)之后才能訪問
.authenticated()方法二:配置一個WebSecurityCustomizer接口進行放行(一般靜態(tài)資源選擇用此放行規(guī)則)
/**
*spring6之后springboot3.0.1
* 資源不用攔截,直接放行,有兩種思路:
*
* 1. web.ignoring().requestMatchers("/login.css"); 這個放行方式,本質(zhì)上。請求將不再經(jīng)過 Spring Security 過濾器鏈
* 2. .requestMatchers("/login.css").permitAll() 這個放行方式,請求還是會經(jīng)過 Spring Security 過濾器鏈,但是不會被攔截
*
* 如果要放行的資源,是靜態(tài)資源,不需要進行 Java 運算的,例如 HTML/CSS/JS/mp3/mp4/圖片,那么可以使用第一種放行方式
* 如果是一個 Java 接口,則建議使用第二種放行方式。
* @return
*/
@Bean
WebSecurityCustomizer webSecurityCustomizer() {
return new WebSecurityCustomizer() {
@Override
public void customize(WebSecurity web) {
//這個也是給資源放行
web.ignoring().requestMatchers("/login.css");
}
};
}
//spring5之前springboot2.7.7
@Bean
WebSecurityCustomizer webSecurityCustomizer() {
return new WebSecurityCustomizer() {
@Override
public void customize(WebSecurity web) {
web.ignoring().antMatchers("css/login.css");
}
};
}注意:如果是靜態(tài)資源就選擇用第二種方式用接口WebSecurityCustomizer進行,但是如果是接口的放行那么就用第一種方式,因為當?shù)卿浀臅r候,我們希望請求可以經(jīng)過SpringSecurity的SecurityContextPersistenceFilter,這個過濾器將會從httpSession中獲得登錄用戶的對象,然后存到SecurityContextHolder里面去,然后SecurityContextHolder本質(zhì)上就是存儲到ThreadLocal里面,保持線程一致性
4.登錄的時候獲取登錄對象(SecurityContextHolder.getContext().getAuthentication())
1.在controller中獲取登錄對象
方式一:通過SecurityContextHolder來獲取
@RestController
//@RequestMapping("/user")
public class UserController {
@GetMapping("/hello")
public String hello(HttpSession session) {
// 獲取當前登錄成功的登錄信息
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// 獲得登錄用戶的名稱
String name = authentication.getName();
//從httpSession里面獲取登錄對象,一般不這樣做,因為我們還是希望可以在同一個線程的
//SecurityContext spring_security_context = (SecurityContext) session.getAttribute("SPRING_SECURITY_CONTEXT");
//String name1 = spring_security_context.getAuthentication().getName();
//System.out.println("name1 = " + name1);
return "hello:" + name;
}
}方式二:通過參數(shù)Principal
@GetMapping("/hello")
public String hello(Principal principal) {
//獲取當前登錄成功的用戶名
String name = principal.getName();
System.out.println("name = " + name);
return "hello";
}方式三:通過參數(shù)HttpSession
2 .在service中獲取登錄的對象
方式一:通過SecurityContextHolder
@Service
public class HelloService {
public void hello() {
SecurityContext securityContext = SecurityContextHolder.getContext();
Authentication authentication = securityContext.getAuthentication();
//獲取當前登錄成功的用戶對象
User user = (User) authentication.getPrincipal();
System.out.println("user = " + user);
}
}方式二:RequestContextHolder.getRequestAttributes()).getRequest()來獲取request來獲取session
@Service
public class HelloService {
public void hello() {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
HttpSession session = request.getSession();
new Thread(new Runnable() {
@Override
public void run() {
//獲取當前請求對象
SecurityContext sc = (SecurityContext) session.getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY);
Authentication authentication1 = sc.getAuthentication();
User user = (User) authentication1.getPrincipal();
System.out.println("fffffffff = " + user);
}
}).start();
}
}5.通過Json來進行登錄驗證
引言:springsecurity是通過key-value的形式獲取登錄的賬戶和密碼的
方式一:重寫UsernamePasswordAuthenticationFilter類里的attemptAuthentication方法
step01-重寫attemptAuthentication方法
public class MyJsonFilter extends UsernamePasswordAuthenticationFilter {
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (!request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
if (request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE) || request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_UTF8_VALUE)) {
//此時就認為請求參數(shù)是 JSON 形式的
User user = null;
try {
user = new ObjectMapper().readValue(request.getInputStream(), User.class);
} catch (IOException e) {
e.printStackTrace();
}
String username = user.getUsername();
username = (username != null) ? username.trim() : "";
String password = user.getPassword();
password = (password != null) ? password : "";
UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username,
password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
} else {
return super.attemptAuthentication(request, response);
}
}
}step02-注入到spring容器中
@Configuration
public class SecurityConfig {
@Autowired
UserService userService;
/**
* 這個過濾器,替代的是 UsernamePasswordAuthenticationFilter,所以我們之前關(guān)于表單的配置,現(xiàn)在都要配置給 MyJsonFilter 才會生效
*
* @return
*/
@Bean
MyJsonFilter myJsonFilter() {
MyJsonFilter myJsonFilter = new MyJsonFilter();
// myJsonFilter.setFilterProcessesUrl();
// myJsonFilter.setUsernameParameter();
// myJsonFilter.setPasswordParameter();
myJsonFilter.setAuthenticationSuccessHandler((req, resp, auth) -> {
resp.getWriter().write("success");
});
myJsonFilter.setAuthenticationFailureHandler(new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
}
});
myJsonFilter.setAuthenticationManager(authenticationManager());
return myJsonFilter;
}
/**
* 這個是一個認證管理器
* @return
*/
@Bean
AuthenticationManager authenticationManager() {
//DaoAuthenticationProvider 這個對象負責數(shù)據(jù)庫密碼的校驗
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
daoAuthenticationProvider.setUserDetailsService(userService);
ProviderManager manager = new ProviderManager(daoAuthenticationProvider);
return manager;
}
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/**")
.authenticated()
.and()
.csrf().disable();
http.formLogin()
//如果只配置登錄頁面,那么默認情況下,登錄接口也是 /login.html
.permitAll();
//新加一個過濾器進來,將之放到 UsernamePasswordAuthenticationFilter 過濾器所在的位置
http.addFilterAt(myJsonFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}方式二:通過將json的值直接放入到UsernamePasswordAuthenticationToken中,
step01-首先在配置類中獲取AuthenticationManager
@Configuration
public class SecurityConfig {
//獲取的是登錄成功的用戶名
@Autowired
UserService userService;
//和shiro中的安全管理器DefaultWebSecurityManager相似,UserDetailsService相當于shiro的Realm
/**
* 這個是一個認證管理器
* @return
*/
@Bean
AuthenticationManager authenticationManager() {
//調(diào)用AuthenticationProvider的子類DaoAuthenticationProvider來驗證。
//默認實例化AuthenticationProvider的一個實現(xiàn):DaoAuthenticationProvider。DaoAuthenticationProvider通過接口UserDetailsService的實現(xiàn)類從內(nèi)存或DB中獲取用戶信息UserDetails(UserDetails十分類似Authentication,也是一個接口,但是與Authentication用途不同,不要搞混)。DaoAuthenticationProvider通過函數(shù)authenticate比較入?yún)uthentication與UserDetails是否相符,來判斷用戶是否可以登錄。如果相符,會將獲得的UserDetails中的信息補全到一個Authentication實現(xiàn)類,并將該實現(xiàn)類作為認證實體返回。以后便可以通過當前上下文的認證實體Authentication獲取當前登錄用戶的信息。
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userService);
//調(diào)用AuthenticationManager的實現(xiàn)類ProviderManager
ProviderManager pm = new ProviderManager(provider);
return pm;
}
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/login").permitAll()
.antMatchers("/**")
.authenticated()
.and()
.csrf().disable();
return http.build();
}step02-接口中直接獲取json
@RestController
public class LoginController {
//設(shè)定一個json的登錄參數(shù),因為底層我們是根據(jù)key-value登錄的
@Autowired
AuthenticationManager authenticationManager;
@PostMapping("/doLogin")
//@RequestBody是json的參數(shù)
public String login(@RequestBody User user) {
UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(user.getUsername(),user.getPassword());
//執(zhí)行認證操作
try {
//參數(shù) authRequest 是一個未經(jīng)認證的 authentication,而方法的返回值是一個認證后的 authentication
Authentication authentication = authenticationManager.authenticate(authRequest);
SecurityContextHolder.getContext().setAuthentication(authentication);
return "success";
} catch (AuthenticationException e) {
e.printStackTrace();
return e.getMessage();
}
}
}@RestController
public class LoginController {
@Autowired
AuthenticationManager authenticationManager;
@PostMapping("/login")
public Map<String, Object> login(@RequestBody User user) throws IOException {
UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.unauthenticated(user.getUsername(), user.getPassword());
try {
//這里就是執(zhí)行具體的認證操作
//參數(shù)是一個未經(jīng)認證的 Authentication 對象,而 authenticate 方法返回值則是一個經(jīng)過認證的 Authentication 的對象
Authentication auth = authenticationManager.authenticate(token);
//將認證后的 auth 對象存入 SecurityContextHolder 中
SecurityContextHolder.getContext().setAuthentication(auth);
//獲取當前登錄成功的用戶對象
User authUser = (User) auth.getPrincipal();
authUser.setPassword(null);
Map<String, Object> map = new HashMap<>();
map.put("status", 200);
map.put("message", "登錄成功");
map.put("data", authUser);
return map;
} catch (AuthenticationException e) {
Map<String, Object> map = new HashMap<>();
map.put("status", 500);
map.put("message", "登錄失敗");
if (e instanceof BadCredentialsException) {
map.put("message", "用戶名或者密碼輸入錯誤,登錄失敗");
} else if (e instanceof AccountExpiredException) {
map.put("message", "賬戶過期,登錄失敗");
} else if (e instanceof CredentialsExpiredException) {
map.put("message", "密碼過期,登錄失敗");
} else if (e instanceof DisabledException) {
map.put("message", "賬戶被禁用,登錄失敗");
} else if (e instanceof LockedException) {
map.put("message", "賬戶被鎖定,登錄失敗");
}
return map;
}
}
}
6.注銷,未登錄訪問的處理器
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/login").permitAll()
.antMatchers("/**")
.authenticated()
.and()
.csrf().disable()
//開啟注銷登錄的配置
.logout()
//第一個參數(shù)是訪問路徑,第二個是請求的方法
.logoutRequestMatcher(new AntPathRequestMatcher("/a", "POST"))
//注銷成功后的回調(diào)
.logoutSuccessHandler((req, resp, auth) -> {
try {
resp.getWriter().write("/a logout success");
} catch (IOException e) {
e.printStackTrace();
}
})
//是否清除認證信息(清除 SecurityContextHolder 中的信息,默認是true)
.clearAuthentication(true)
//是否銷毀 HttpSession
.invalidateHttpSession(true)
.permitAll()
.and()
//前端出現(xiàn)403,沒有登錄,禁止訪問
.exceptionHandling()
//配置未認證訪問的處理器
.authenticationEntryPoint(new AuthenticationEntryPoint() {
/**
* 當用戶沒有認證,但是卻訪問了一個需要認證之后才能訪問的接口,那么就會觸發(fā)該方法
* @param request
* @param response
* @param authException
* @throws IOException
* @throws ServletException
*/
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
Map<String, Object> map = new HashMap<>();
map.put("status", 500);
map.put("message", "尚未登錄,請登錄");
//設(shè)置UTF-8
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
response.getWriter().write(new ObjectMapper().writeValueAsString(map));
}
});
return http.build();
}7.加密(PasswordEncoder)
1.密碼的加密匹配器
以下是原理
接口(PasswordEncoder)自帶鹽
常用的實現(xiàn)類:DelegatingPasswordEncoder(代理密碼加密)通過PasswordEncoderFactories來調(diào)用,默認使用的是BCryptPasswordEncoder
public interface PasswordEncoder {
//將傳來的密碼,加鹽加密生成編碼,例如當你注冊的時候,將明文轉(zhuǎn)成密文
String encode(CharSequence rawPassword);
//判斷密碼是否正確,第一個參數(shù)是明文密碼,第二個參數(shù)是加鹽加密密碼
boolean matches(CharSequence rawPassword, String encodedPassword);
//升級密碼
default boolean upgradeEncoding(String encodedPassword) {
return false;
}
}
注意**:1.你用什么實現(xiàn)類進行加鹽加密的就得用什么實現(xiàn)類來進行比對
2.當沒有配置加密匹配器的時候,就會用DelegatingPasswordEncoder密碼匹配器進行調(diào)用默認的密碼匹配器SCryptPasswordEncoder,因為它底層可以給其余的10種密碼匹配器取名,我們只需要在數(shù)據(jù)庫的密碼中前面寫:例如{noop}123,就是使用NoopPasswordEncode密碼匹配器
//spring5+springboot2.7.7+jdk8
public final class PasswordEncoderFactories {
private PasswordEncoderFactories() {
}
public static PasswordEncoder createDelegatingPasswordEncoder() {
String encodingId = "bcrypt";
Map<String, PasswordEncoder> encoders = new HashMap();
encoders.put(encodingId, new BCryptPasswordEncoder());
encoders.put("ldap", new LdapShaPasswordEncoder());
encoders.put("MD4", new Md4PasswordEncoder());
encoders.put("MD5", new MessageDigestPasswordEncoder("MD5"));
encoders.put("noop", NoOpPasswordEncoder.getInstance());
encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
encoders.put("scrypt", new SCryptPasswordEncoder());
encoders.put("SHA-1", new MessageDigestPasswordEncoder("SHA-1"));
encoders.put("SHA-256", new MessageDigestPasswordEncoder("SHA-256"));
encoders.put("sha256", new StandardPasswordEncoder());
encoders.put("argon2", new Argon2PasswordEncoder());
//第一個參數(shù),表示的是默認的密碼匹配器
return new DelegatingPasswordEncoder(encodingId, encoders);
}
}//spring6+springboot3.0.1+jdk17
public final class PasswordEncoderFactories {
private PasswordEncoderFactories() {
}
public static PasswordEncoder createDelegatingPasswordEncoder() {
String encodingId = "bcrypt";
Map<String, PasswordEncoder> encoders = new HashMap();
encoders.put(encodingId, new BCryptPasswordEncoder());
encoders.put("ldap", new LdapShaPasswordEncoder());
encoders.put("MD4", new Md4PasswordEncoder());
encoders.put("MD5", new MessageDigestPasswordEncoder("MD5"));
encoders.put("noop", NoOpPasswordEncoder.getInstance());
encoders.put("pbkdf2", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_5());
encoders.put("pbkdf2@SpringSecurity_v5_8", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8());
encoders.put("scrypt", SCryptPasswordEncoder.defaultsForSpringSecurity_v4_1());
encoders.put("scrypt@SpringSecurity_v5_8", SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8());
encoders.put("SHA-1", new MessageDigestPasswordEncoder("SHA-1"));
encoders.put("SHA-256", new MessageDigestPasswordEncoder("SHA-256"));
encoders.put("sha256", new StandardPasswordEncoder());
encoders.put("argon2", Argon2PasswordEncoder.defaultsForSpringSecurity_v5_2());
encoders.put("argon2@SpringSecurity_v5_8", Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8());
return new DelegatingPasswordEncoder(encodingId, encoders);
}
}step01-配置passwordEncoder類,我們可以使用系統(tǒng)默認的代理密碼匹配,也可以寫自己想要的密碼匹配器,只需要在配置類中修改 DelegatingPasswordEncoder passwordEncoder = new DelegatingPasswordEncoder(encodingId, encoders) ,它會自帶前綴
@Configuration
public class SecurityConfig {
/**
* 配置了這個 Bean 之后,項目中的密碼加密都是用 BCryptPasswordEncoder,那么就不需要額外指定密碼加密方案了
* @return
*/
@Bean
PasswordEncoder passwordEncoder() {
// return new BCryptPasswordEncoder();
String encodingId = "bcrypt";
Map<String, PasswordEncoder> encoders = new HashMap();
encoders.put(encodingId, new BCryptPasswordEncoder());
encoders.put("ldap", new LdapShaPasswordEncoder());
encoders.put("MD4", new Md4PasswordEncoder());
encoders.put("MD5", new MessageDigestPasswordEncoder("MD5"));
encoders.put("noop", NoOpPasswordEncoder.getInstance());
encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
encoders.put("scrypt", new SCryptPasswordEncoder());
encoders.put("SHA-1", new MessageDigestPasswordEncoder("SHA-1"));
encoders.put("SHA-256", new MessageDigestPasswordEncoder("SHA-256"));
encoders.put("sha256", new StandardPasswordEncoder());
encoders.put("argon2", new Argon2PasswordEncoder());
DelegatingPasswordEncoder passwordEncoder = new DelegatingPasswordEncoder(encodingId, encoders);
return passwordEncoder;
}
}2.密碼升級(UserDetailsPasswordService)
原因:當用戶的密碼需要實現(xiàn)升級的時候,也就是想要修改成別的密碼匹配器時或者數(shù)據(jù)庫中可能存在不同的加密匹配器的加密機制,想數(shù)據(jù)庫中的密碼全部升級為統(tǒng)一的密碼加密機制時,就會用到密碼升級
方式一:修改配置類的密碼匹配器
步驟:step01-配置passwordEncoder類
@Configuration
public class SecurityConfig {
/**
* 配置了這個 Bean 之后,項目中的密碼加密都是用 BCryptPasswordEncoder,那么就不需要額外指定密碼加密方案了
* @return
*/
@Bean
PasswordEncoder passwordEncoder() {
// return new BCryptPasswordEncoder();
String encodingId = "bcrypt";
Map<String, PasswordEncoder> encoders = new HashMap();
encoders.put(encodingId, new BCryptPasswordEncoder());
encoders.put("ldap", new LdapShaPasswordEncoder());
encoders.put("MD4", new Md4PasswordEncoder());
encoders.put("MD5", new MessageDigestPasswordEncoder("MD5"));
encoders.put("noop", NoOpPasswordEncoder.getInstance());
encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
encoders.put("scrypt", new SCryptPasswordEncoder());
encoders.put("SHA-1", new MessageDigestPasswordEncoder("SHA-1"));
encoders.put("SHA-256", new MessageDigestPasswordEncoder("SHA-256"));
encoders.put("sha256", new StandardPasswordEncoder());
encoders.put("argon2", new Argon2PasswordEncoder());
DelegatingPasswordEncoder passwordEncoder = new DelegatingPasswordEncoder(encodingId, encoders);
return passwordEncoder;
}
}step02-需要在service接口中繼承UserDetailsPasswordService接口,實現(xiàn)UserDetailsPasswordService接口中的updatePassword方法
/**
* 當用戶登錄的時候,系統(tǒng)會自動判斷用戶密碼是否需要升級
* @param user:登錄時候的用戶
* @param newPassword 新的加密后的密碼
* @return
*/
@Override
public UserDetails updatePassword(UserDetails user, String newPassword) {
//條件構(gòu)造器
UpdateWrapper<User> uw = new UpdateWrapper<>();
//將修改為新密碼
uw.lambda().set(User::getPassword, newPassword)
//是根據(jù)哪個用戶id
.eq(User::getId, ((User) user).getId());
//sql修改
boolean update = update(uw);
//((User) user).setPassword(newPassword);
return user;
}@Service
public class UserService implements UserDetailsService, UserDetailsPasswordService {
@Autowired
UserMapper userMapper;
/**
* 根據(jù)用戶名稱查詢用戶對象,當用戶登錄的時候,這個方法會被自動觸發(fā)
*
* @param username
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userMapper.loadUserByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("用戶名不存在");
}
return user;
}
/**
* 當用戶每次登錄的時候,都會去檢查用戶的密碼是否需要升級,如果需要升級,則當前方法會被觸發(fā)。
* 升級的情況:
* 1. 無論是系統(tǒng)默認配置的 DelegatingPasswordEncoder 還是開發(fā)者自己配置的 DelegatingPasswordEncoder,DelegatingPasswordEncoder 中都有一個默認的密碼加密方案。那么當用戶登錄的時候,系統(tǒng)就會去檢查用戶當前的密碼加密方案是否為 DelegatingPasswordEncoder 中默認的加密方案,如果不是,則當前方法就會被觸發(fā),系統(tǒng)會進行密碼升級,將現(xiàn)有的密碼加密方案改為 DelegatingPasswordEncoder 中默認的密碼加密方案。
* 2. 同一種密碼加密方案也可能存在升級的情況,例如 BCryptPasswordEncoder 中,可以設(shè)置密碼的強度,密碼強度不同,也會導致密碼自動升級
* @param user:實體類的User
* @param newPassword:新的加密后的密碼
* @return
*/
@Override
public UserDetails updatePassword(UserDetails user, String newPassword) {
User u = (User) user;
u.setPassword(newPassword);
//sql語句:修改數(shù)據(jù)庫的密碼
userMapper.updatePassword(u);
return user;
}
}step03-測試
如果數(shù)據(jù)庫密碼是{noop}123,就是升級為{bcrypt},系統(tǒng)默認的
方式二:修改密碼匹配器的強度(參數(shù))
@Bean
PasswordEncoder passwordEncoder() {
String encodingId = "bcrypt";
Map<String, PasswordEncoder> encoders = new HashMap();
encoders.put(encodingId, new BCryptPasswordEncoder(11));
encoders.put("ldap", new LdapShaPasswordEncoder());
encoders.put("MD4", new Md4PasswordEncoder());
encoders.put("MD5", new MessageDigestPasswordEncoder("MD5"));
encoders.put("noop", NoOpPasswordEncoder.getInstance());
encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
encoders.put("scrypt", new SCryptPasswordEncoder());
encoders.put("SHA-1", new MessageDigestPasswordEncoder("SHA-1"));
encoders.put("SHA-256", new MessageDigestPasswordEncoder("SHA-256"));
encoders.put("sha256", new StandardPasswordEncoder());
encoders.put("argon2", new Argon2PasswordEncoder());
DelegatingPasswordEncoder passwordEncoder = new DelegatingPasswordEncoder(encodingId, encoders);
return passwordEncoder;
?
}8.remember me

1.remember me的基本使用
1.與shiro的區(qū)別是:系統(tǒng)重啟了,remember me還是存在
2.springboot3.0.1才有算法名

step01-接口
@RestController
public class HelloController {
/**
* 這里的參數(shù) Principal 就表示當前登錄成功的用戶對象
* 設(shè)置這個接口remember me或者登錄都可以訪問
* @param principal
* @return
*/
@GetMapping("/hello")
public String hello(Principal principal) {
return "hello";
}
/**
* 設(shè)置這個接口只有通過Rememeberme才能夠訪問
* @return
*/
@GetMapping("/rm")
public String rm(){
return "rm";
}
/**
* 設(shè)置是賬戶密碼登錄時才可以登錄
* @return
*/
@GetMapping("/hello2")
public String hello2(){
return "密碼登錄的";
}
}step02-配置過濾器鏈
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeRequests()
//只能通過remember me才能夠訪問
.antMatchers("/rm").rememberMe()
//只有時賬戶密碼登錄才能夠訪問
.antMatchers("/hello2").fullyAuthenticated()
.anyRequest()
.authenticated()
.and()
.formLogin()
.permitAll()
.and()
.rememberMe()
//隨便給一個key,以后系統(tǒng)重啟了,remember me還是存在的,和shiro不太一樣
.key("a")
.and()
.csrf()
.disable();
return http.build();
}2.持續(xù)化令牌

將用戶登錄的用戶名,series,token存儲起來,也是通過series,token來確定有沒有被別人登陸了,每當自動登錄的時候,token就會改變一次,提高安全性,當注銷的時候存入到數(shù)據(jù)庫的信息就會刪除
step01-在數(shù)據(jù)庫中執(zhí)行上面的sql語句,創(chuàng)建表persistent_logins
step02-在配置文件中重新配置PersistentTokenBasedRememberMeServices里的PersistentTokenRepository

@Configuration
public class SecurityConfig {
@Autowired
DataSource dataSource;
@Bean
JdbcTokenRepositoryImpl jdbcTokenRepository() {
JdbcTokenRepositoryImpl repository = new JdbcTokenRepositoryImpl();
repository.setDataSource(dataSource);
return repository;
}
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests()
// /rm 必須是以 RememberMe 的方式登錄才可以訪問
.requestMatchers("/rm").rememberMe()
//這個表示必須用戶名密碼登錄,才能訪問 /hello2
.requestMatchers("/hello2").fullyAuthenticated()
.requestMatchers("/**")
.authenticated()
.and()
.formLogin()
.permitAll()
.and()
.rememberMe()
.key("123")
//配置PersistentTokenRepository
.tokenRepository(jdbcTokenRepository())
.and()
.csrf().disable();
return http.build();
}
}9.會話管理
引言:如果在配置文件中設(shè)置了最大登錄的是1個時,每一次登錄的時候都會將另一個擠下去,或者登不上,是根據(jù)User實體類中的equals和hashCode來判斷用戶是否登錄
step01-實體類User寫equals和hashCode的方法
public class User implements UserDetails {
private Integer id;
private String username;
private String password;
private String address;
private Boolean enabled;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return Objects.equals(username, user.username);
}
@Override
public int hashCode() {
return Objects.hash(username);
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public void setUsername(String username) {
this.username = username;
}
public void setPassword(String password) {
this.password = password;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public void setEnabled(Boolean enabled) {
this.enabled = enabled;
}
/**
* 獲取當前用戶的權(quán)限/角色
* @return
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
/**、
* 獲取用戶密碼
* @return
*/
@Override
public String getPassword() {
return password;
}
/**
* 獲取用戶名
* @return
*/
@Override
public String getUsername() {
return username;
}
/**
* 賬戶是否沒有過期
* @return
*/
@Override
public boolean isAccountNonExpired() {
return true;
}
/**
* 賬戶是否沒有被鎖定
* @return
*/
@Override
public boolean isAccountNonLocked() {
return true;
}
/**
* 密碼是否沒有過期
* @return
*/
@Override
public boolean isCredentialsNonExpired() {
return true;
}
/**
* 賬戶是否可用
* @return
*/
@Override
public boolean isEnabled() {
return enabled;
}
}step02-添加會話的配置信息
判斷用戶是可以同時在線幾個是根據(jù)Map<User,List<Session>>
@Configuration
public class SecurityConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.permitAll()
.and()
.sessionManagement()
/**
*Map<User,List<Session>>
* 底層邏輯是有一個映射表,Map,map 的 key 是用戶對象,value 則是一個集合,集合中保存了 session
*/
//設(shè)置同一個用戶的 session 并發(fā)數(shù)
.maximumSessions(1)
//當達到最大并發(fā)數(shù)的時候,是否禁止新的登錄
.maxSessionsPreventsLogin(true)
.and()
//防止會話攻擊,(默認也是這個配置)
.sessionFixation().migrateSession()
.and()
.csrf()
.disable();
return http.build();
}
/**
* 當注銷的時候,map里面還存著用戶的session的,我們得通過這個bean來觸發(fā)監(jiān)聽器
* 這是一個事件發(fā)布器,這個會自動將 HttpSession 銷毀的事件發(fā)布出去,發(fā)布之后會被 map 感知到,進而移除 map 中的 session
*
* @return
*/
@Bean
HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}
}10.防止csrf攻擊
起因:黑客利用瀏覽器存在cookie,當你登錄時,然后又訪問了黑客訪問的頁面的端口,當你訪問時就可以直接盜竊你的信息,甚至轉(zhuǎn)賬。(小程序和軟件不存在這個問題)
解決:我們可以開啟csrf,當我們自己訪問post,delete,put也無法訪問,黑客更加無法訪問了,這是我們只需要在瀏覽器上添加過濾器給予你的csrf字符串,你就可以訪問,黑客是無法獲取csrf字符串的(可以獲取cookie是因為瀏覽器存著,然后訪問時直接調(diào)用)
情況一:csrf過濾器隨機生成的值,我們要以參數(shù)的形式請求上去
//提交_csrf的參數(shù)過去,就可以訪問了
<form action="/transfer" method="post">
<input type="text" name="username">
<input type="text" name="money">
<input type="hidden" name="_csrf" th:value="${_csrf.token}">
<input type="submit" value="轉(zhuǎn)賬">
</form>情況二:用到ajax來進行訪問,將cookie里面的參數(shù)提取出來,然后發(fā)送請求上去
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<!-- <script src="jquery-3.6.3.min.js"></script>-->
<!-- <script src="jquery.min.js"></script>
<script src="jquery.cookie.js"></script>-->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-cookie/1.4.1/jquery.cookie.min.js"></script>
</head>
<body>
<input type="text" id="username" value="zhangsan">
<input type="text" id="password" value="123">
<button onclick="doTransfer()">登錄</button>
<div id="result"></div>
<script>
function doTransfer() {
let token = $.cookie('XSRF-TOKEN');
console.log("token>>>>", token)
// _csrf: token:將配置中的
$.post("/login", {username: $("#username").val(), password: $("#password").val(), _csrf: token},function (msg) {
$("#result").html(msg);
});
}
</script>
</body>
</html>@Configuration
public class SecurityConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.permitAll()
.and()
//添加了csrf安全過濾器,因為黑客可以通過你瀏覽的網(wǎng)址獲取你登錄時候的cookie,從而導致你的賬戶被獲取,你可以添加csrf自動創(chuàng)建的字符串,加在后面,即便黑客獲取cookie,但是無法獲取你的字符串也沒用,因為是看你請求的參數(shù)里面有沒有csrf參數(shù)的,不是看你的coken
//這個將csrf存放在cookie里面的,然后通過js來將cookie的值讀取出來,找到cookie中的_csrf中的值,作為普通請求的參數(shù)攜帶上。服務(wù)端是在你請求的參數(shù)上找有沒有_csrf的,并不是在cookie中解析尋找的
.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
return http.build();
}
}11.多個過濾器鏈
1.多個用戶訪問是不同的過濾器鏈
step01-接口
@RestController
public class HelloController {
/**
* 我希望,這個訪問這個接口的請求,需要經(jīng)過 ConcurrentSessionFilter 過濾器
* @return
*/
@GetMapping("/user/hello")
public String user() {
return "hello uesr";
}
/**
* 訪問這個請求的地址,不需要經(jīng)過 CsrfFilter
* @return
*/
@GetMapping("/admin/hello")
public String admin() {
return "hello admin";
}
@GetMapping("/hello")
public String hello() {
return "hello";
}
}step02-配置類
@Configuration
public class SecurityConfig {
@Bean
UserDetailsService us1() {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("zhangsan").password("{noop}123").roles("admin").build());
return manager;
}
@Bean
UserDetailsService us2() {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("lisi").password("{noop}123").roles("user").build());
return manager;
}
/**
* 這個就是配置的第一個過濾器鏈
* @param http
* @return
* @throws Exception
*/
@Bean
SecurityFilterChain securityFilterChain01(HttpSecurity http) throws Exception {
//這個地方是在配置過濾器鏈,這個 antMatcher 方法表示如果你的請求格式是 /user/** 的話,那么就會進入到當前鏈中
http.antMatcher("/user/**")
.authorizeRequests()
.antMatchers("/user/**").hasAuthority("user")
.anyRequest().authenticated()
.and()
.formLogin()
.loginProcessingUrl("/user/login")
.successHandler((req, resp, auth) -> {
resp.getWriter().write("success01");
})
.failureHandler((req, resp, e) -> {
resp.getWriter().write("failure01");
})
.permitAll()
.and()
.sessionManagement()
.maximumSessions(1)
.and()
.and()
.csrf().disable()
//每一個過濾器鏈,可以對應(yīng)不同的數(shù)據(jù)源,如果我們不為每一個過濾器鏈設(shè)置單獨的數(shù)據(jù)源,那么也可以直接向 Spring 容器中注冊一個數(shù)據(jù)源,那么這個數(shù)據(jù)源就是公用的。
.userDetailsService(us1());
return http.build();
}
/**
* 這個就是配置的第二個過濾器鏈
* @param http
* @return
* @throws Exception
*/
@Bean
SecurityFilterChain securityFilterChain02(HttpSecurity http) throws Exception {
//這個地方是在配置過濾器鏈,這個 antMatcher 方法表示如果你的請求格式是 /admin/** 的話,那么就會進入到當前鏈中
http.antMatcher("/admin/**")
.authorizeRequests()
.antMatchers("/admin/**").hasAuthority("admin")
.anyRequest().authenticated()
.and()
.formLogin()
.loginProcessingUrl("/admin/login")
.successHandler((req, resp, auth) -> {
resp.getWriter().write("success02");
})
.failureHandler((req, resp, e) -> {
resp.getWriter().write("failure02");
})
.permitAll()
.and()
.csrf().disable()
.userDetailsService(us2());
return http.build();
}
@Bean
SecurityFilterChain securityFilterChain03(HttpSecurity http) throws Exception {
//任何用戶都可以訪問
http.antMatcher("/**")
.authorizeRequests()
.anyRequest().authenticated()
.and()
.csrf().disable();
return http.build();
}
}2.多個數(shù)據(jù)源對應(yīng)的過濾器鏈
多個數(shù)據(jù)源

step01-配置多個數(shù)據(jù)源對應(yīng)的登錄UserDetailsService(這個相當于Realm,用來獲取前端的username)
@Service
public class UserService2207 implements UserDetailsService {
@Autowired
UserMapper userMapper;
/**
* 根據(jù)用戶名稱查詢用戶對象,當用戶登錄的時候,這個方法會被自動觸發(fā)
*
* @param username
* @return
* @throws UsernameNotFoundException
*/
@Override
@Ds("master")
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userMapper.loadUserByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("用戶名不存在");
}
return user;
}
}@Service
public class UserServiceJpaDemo implements UserDetailsService {
@Autowired
UserMapper userMapper;
/**
* 根據(jù)用戶名稱查詢用戶對象,當用戶登錄的時候,這個方法會被自動觸發(fā)
*
* @param username
* @return
* @throws UsernameNotFoundException
*/
@Override
@Ds("slave")
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userMapper.loadUserByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("用戶名不存在");
}
return user;
}
}step02-配置類,給對應(yīng)的service匹配對應(yīng)的過濾器鏈
@Configuration
public class SecurityConfig {
@Autowired
UserService2207 userService2207;
@Autowired
UserServiceJpaDemo userServiceJpaDemo;
/**
* 提供一個這樣的 Bean,就是提供了一個過濾器鏈,這個過濾器鏈中約 10 多個過濾器
*
* @param http
* @return
* @throws Exception
*/
@Bean
SecurityFilterChain phoneSecurityFilterChain(HttpSecurity http) throws Exception {
http
//這個表示配置當前過濾器鏈的攔截規(guī)則
.securityMatcher("/phone/**")
// UserDetailsService 分為兩種,一種是全局的,另外一種是局部的,如果我們不給每一個過濾器鏈單獨設(shè)置 UserDetailsService,則都使用的是全局的
.userDetailsService(userService2207)
.authorizeHttpRequests().requestMatchers("/**")
.authenticated()
.and()
.formLogin()
.loginProcessingUrl("/phone/login")
.successHandler((req,resp,auth)->{
resp.getWriter().write("phone login success");
})
.failureHandler((req,resp,e)->{
resp.getWriter().write(e.getMessage());
})
.permitAll()
.and()
.csrf().disable();
return http.build();
}
@Bean
SecurityFilterChain webSecurityFilterChain(HttpSecurity http) throws Exception {
http
//這個表示配置當前過濾器鏈的攔截規(guī)則
.securityMatcher("/web/**")
.userDetailsService(userServiceJpaDemo)
.authorizeHttpRequests().requestMatchers("/**")
.authenticated()
.and()
.formLogin()
.successHandler((req,resp,auth)->{
resp.getWriter().write("web login success");
})
.loginPage("/web/login")
.loginProcessingUrl("/web/login")
.permitAll();
return http.build();
}
}12.JWT登錄(無狀態(tài)登錄)
有狀態(tài)登錄攜帶的sessionid是沒有含義的,就只是隨機id;無狀態(tài)的token是有含義的
1.httpBasic
只需要將formLogin()寫成httpBasic()就可以實現(xiàn)了,但是有缺點:1.注銷問題,無法注銷 2.續(xù)期問題,不會自動重置時間 3.密碼重置 4.把密碼用戶信息放在請求頭,不安全,每一次調(diào)用一個接口都會顯示,因為是無狀態(tài),它需要無時無刻校驗
@Configuration
public class SecurityConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests()
.requestMatchers("/**")
.authenticated()
.and()
.httpBasic();
return http.build();
}
}2./jwtk/jjwt
step01-jwt專屬·依賴

step02-配置類,如果成功登錄就生成JWT字符串
@Configuration
public class SecurityConfig {
//可以隨機是一段字符串
public static final Key KEY = Keys.hmacShaKeyFor("jfkdafljksajklf75894237uafkldsjfalkj8134784395jfkdafljksajklf75894237uafkldsjfalkj8134784395jfkdafljksajklf75894237uafkldsjfalkj8134784395jfkdafljksajklf75894237uafkldsjfalkj8134784395jfkdafljksajklf75894237uafkldsjfalkj8134784395jfkdafljksajklf75894237uafkldsjfalkj8134784395jfkdafljksajklf75894237uafkldsjfalkj8134784395".getBytes());
//引入寫好的過濾器
@Autowired
JwtFilter jwtFilter;
/**
* 配置 Spring Security 的過濾器鏈
*
* @return
*/
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
//因為jwtFilter中有用戶名,密碼,權(quán)限的設(shè)置在UsernamePasswordAuthentication該類,所以在它前面執(zhí)行
http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
http.authorizeHttpRequests()
.requestMatchers("/**")
.authenticated()
.and()
//關(guān)閉 session 的創(chuàng)建,這樣就不會產(chǎn)生sessionid了,實現(xiàn)無狀態(tài)訪問
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.NEVER)
.and()
.formLogin()
.successHandler((req, resp, auth) -> {
//保存的數(shù)據(jù)是需要map存儲
Map<String, String> map = new HashMap<>();
//獲取當前登錄成功的用戶角色,是在User實體類的權(quán)限角色方法中獲取的
Collection<? extends GrantedAuthority> authorities = auth.getAuthorities();
// ROLE_admin,ROLE_user,使用Stream流將獲取的權(quán)限以,隔開
map.put("authorities", authorities.stream().map(a -> a.getAuthority()).collect(Collectors.joining(",")));
//生成 JWT 字符串
String jws = Jwts.builder()
//配置主題
.setSubject(auth.getName())
//過期時間,七天之后過期
.setExpiration(new Date(System.currentTimeMillis() + 7 * 24 * 60 * 60 * 1000))
//令牌簽發(fā)時間
.setIssuedAt(new Date())
//這個里邊可以保存比較豐富的數(shù)據(jù)類型
.setClaims(map)
//將Key放入
.signWith(KEY).compact();
resp.getWriter().write(jws);
})
.failureHandler((req, resp, e) -> {
resp.getWriter().write("login error>>>" + e.getMessage());
})
.and()
.csrf().disable()
//拋出的異??梢栽诖双@得
.exceptionHandling().accessDeniedHandler(new AccessDeniedHandler() {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
response.getWriter().write(accessDeniedException.getMessage());
}
});
return http.build();
}
}step03-寫一個過濾器,用戶每申請一個請求就觸碰這個類來解析JWT字符串
/**
* @author baize
* @date 2023/1/5
* @site www.qfedu.com
* <p>
* 這里專門用來解析 JWT 字符串,以后每個請求到達的時候都從這里去解析 JWT 字符串
*/
@Component
public class JwtFilter extends GenericFilterBean {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) servletRequest;
//當是登錄接口的時候就不需要執(zhí)行下面的解析,而是直接往下別的過濾器執(zhí)行
if (req.getRequestURI().contains("/login")) {
//說明是一個登錄請求
filterChain.doFilter(servletRequest, servletResponse);
return;
}
//獲取請求頭參數(shù)是Authorization來獲取jwt字符串
String authorization = req.getHeader("Authorization");
//如果請求頭中沒有就在請求參數(shù)中獲取
if (authorization == null || "".equalsIgnoreCase(authorization)) {
authorization = req.getParameter("token");
}
//如果jwt字符串不為空
if (authorization != null && !"".equals(authorization)) {
//因為jwt字符串的開頭是以Bearer 存在的
authorization = authorization.replace("Bearer ", "");
Jws<Claims> claimsJws = null;
try {
//開始解析,將Key和jwt字符串放入
claimsJws = Jwts.parserBuilder().setSigningKey(SecurityConfig.KEY).build().parseClaimsJws(authorization);
} catch (Exception e) {
servletResponse.getWriter().write("jwt error");
return;
}
//獲取配置文件中存儲的豐富的數(shù)據(jù)類型的對象
Claims claims = claimsJws.getBody();
//獲取存儲在map中的權(quán)限
// ROLE_admin,ROLE_user
String authorities = (String) claims.get("authorities");
//將ROLE_admin,ROLE_user轉(zhuǎn)為集合
List<SimpleGrantedAuthority> list = Arrays.stream(authorities.split(",")).map(a -> new SimpleGrantedAuthority(a)).collect(Collectors.toList());
//獲取用戶名
String username = claims.getSubject();
//將獲取的用戶名和權(quán)限存入到該類中
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, null, list);
SecurityContextHolder.getContext().setAuthentication(token);
filterChain.doFilter(servletRequest, servletResponse);
} else {
// throw new AuthenticationServiceException("jwt error");
servletResponse.getWriter().write("jwt error");
}
}
}step04-在配置類中的拋出異常顯示的權(quán)限不夠可以用全局異常機制
@RestControllerAdvice
public class GlobalException {
@ExceptionHandler(AuthenticationServiceException.class)
public String authenticationException(AuthenticationServiceException e) {
return e.getMessage();
}
}13.用戶名密碼分開提示
1.創(chuàng)造一個類,讓該類來返回登錄成功或者失敗的信息
public class RespBean {
private Integer status;
private String msg;
private Object data;
public static RespBean ok(String msg, Object data) {
return new RespBean(200, msg, data);
}
public static RespBean ok(String msg) {
return new RespBean(200, msg, null);
}
public static RespBean error(String msg, Object data) {
return new RespBean(500, msg, data);
}
public static RespBean error(String msg) {
return new RespBean(500, msg, null);
}
private RespBean() {
}
private RespBean(Integer status, String msg, Object data) {
this.status = status;
this.msg = msg;
this.data = data;
}
public Integer getStatus() {
return status;
}
public void setStatus(Integer status) {
this.status = status;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
}2.配置登錄時的配置類
@Configuration
public class SecurityConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
//登陸成功時
.successHandler((req, resp, authentication) -> {
resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
RespBean respBean = RespBean.ok("登錄成功", authentication.getPrincipal());
// 通過json將對象轉(zhuǎn)成String類型傳到前端
String s = new ObjectMapper().writeValueAsString(respBean);
out.write(s);
})
.failureHandler((req, resp, e) -> {
resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
RespBean respBean = null;
if (e instanceof BadCredentialsException) {
respBean = RespBean.error("密碼輸入錯誤,登錄失敗");
} else if (e instanceof UsernameNotFoundException) {
respBean = RespBean.error("用戶名輸入錯誤,登錄失敗");
} else if (e instanceof AccountExpiredException) {
respBean = RespBean.error("賬戶過期,登錄失敗");
} else if (e instanceof LockedException) {
respBean = RespBean.error("賬戶被鎖定,登錄失敗");
} else if (e instanceof CredentialsExpiredException) {
respBean = RespBean.error("密碼過期,登錄失敗");
} else if (e instanceof DisabledException) {
respBean = RespBean.error("賬戶被禁用,登錄失敗");
} else {
respBean = RespBean.error("登錄失敗");
}
String s = new ObjectMapper().writeValueAsString(respBean);
out.write(s);
})
.and()
.csrf()
.disable()
.exceptionHandling()
//當訪問一個需要認證的地址時,會調(diào)用到該方法
.authenticationEntryPoint((req, resp, authException) -> {
resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
RespBean respBean = RespBean.error("尚未登錄,請登錄");
String s = new ObjectMapper().writeValueAsString(respBean);
out.write(s);
})
//當權(quán)限不足的時候,拋出異常,會在這里被捕獲
//即 throw new AccessDeniedException("權(quán)限不足,請聯(lián)系管理員");拋出的異常,在這里處理
.accessDeniedHandler((req, resp, e) -> {
resp.setContentType("application/json;charset=utf-8");
RespBean respBean = RespBean.error(e.getMessage());
resp.getWriter().write(new ObjectMapper().writeValueAsString(respBean));
})
;
return http.build();
}
}如果你想賬戶和密碼分開提示,在.formLogin()上加這段代碼
//對象后置處理器,Spring Security 默認已經(jīng)幫我們處理好了 DaoAuthenticationProvider Bean,該 Bean 即將注冊到 Spring 容器中,這里是一個對象的后置處理器,就是可以在這里對 Bean 的屬性再做一次修改
.withObjectPostProcessor(new ObjectPostProcessor<UsernamePasswordAuthenticationFilter>() {
@Override
public <O extends UsernamePasswordAuthenticationFilter> O postProcess(O object) {
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
daoAuthenticationProvider.setHideUserNotFoundExceptions(false);
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("zhangsan").password("{noop}123").roles("admin").build());
daoAuthenticationProvider.setUserDetailsService(manager);
object.setAuthenticationManager(new ProviderManager(daoAuthenticationProvider));
return object;
}
})二.權(quán)限與角色
1.權(quán)限和角色的配置
step01-設(shè)置接口,每一種權(quán)限對應(yīng)不同的接口訪問
@RestController
public class HelloController {
/**
* 只要用戶登錄了就可以訪問這個接口
*
* @return
*/
@GetMapping("/hello")
public String hello() {
return "hello";
}
/**
* 用戶登錄之后,必須具備 admin 這個角色才可以訪問
* @return
*/
@GetMapping("/admin/hello")
public String admin() {
return "hello admin";
}
/**
* 用戶登錄之后,必須具備 user 角色才可以訪問
* @return
*/
@GetMapping("/user/hello")
public String user() {
return "hello user";
}
}step02-在UserService中,因為在登錄的時候就要查看有什么權(quán)限了,所以在這里設(shè)置好權(quán)限返回給User實體類
@Service
public class UserService implements UserDetailsService {
@Autowired
UserMapper userMapper;
/**
* 根據(jù)用戶名稱查詢用戶對象,當用戶登錄的時候,這個方法會被自動觸發(fā)
*
* @param username
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userMapper.loadUserByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("用戶名不存在");
}
user.setRoles(userMapper.getUserRolesByUserId(user.getId()));
return user;
}
}mapper接口
@Mapper
public interface UserMapper {
User loadUserByUsername(String username);
List<Role> getUserRolesByUserId(Integer uid);
}<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.qfedu.authorize.mapper.UserMapper">
<select resultType="com.qfedu.authorize.model.User" id="loadUserByUsername">
select * from user where username=#{username};
</select>
<select id="getUserRolesByUserId" resultType="com.qfedu.authorize.model.Role">
SELECT r.* FROM role r,user_role ur WHERE r.`id`=ur.`rid` AND ur.`uid`=#{uid}
</select>
</mapper>step03-定義Role的實體類,User實體類中可能有多個Role還有配置權(quán)限的方法
public class Role {
private Integer id;
private String name;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}public class User implements UserDetails {
private Integer id;
private String username;
private String password;
private String address;
private Boolean enabled;
//一個用戶可能有多個角色
private List<Role> roles;
/**
* 獲取當前用戶的權(quán)限/角色
*
* 當系統(tǒng)需要知道當前用戶的角色/權(quán)限的時候,都是調(diào)用這個方法。這塊跟 Shiro 不同,在 Spring Security 中,用戶的權(quán)限和角色都是通過這個方法來返回的
*
* @return
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
// List<SimpleGrantedAuthority> authorities = new ArrayList<>();
// for (Role role : roles) {
// SimpleGrantedAuthority authority = new SimpleGrantedAuthority(role.getName());
// authorities.add(authority);
// }
return roles.stream().map(r -> new SimpleGrantedAuthority(r.getName())).collect(Collectors.toList());
}step04-配置角色
注意:角色記得要在數(shù)據(jù)庫的前面加Role_

@Configuration
public class SecurityConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeRequests()
//訪問 /admin/** 必須具備 admin 這個角色
//無論是 hasRole 還是 hasAuthority,最終調(diào)用的權(quán)限判斷方法都是同一個,僅僅只是從參數(shù)上去區(qū)分到底是權(quán)限還是角色,如果參數(shù)是以 ROLE_ 開始的,則表示這是一個角色;否則就是一個權(quán)限字符串
.antMatchers("/admin/**").hasRole("admin")
//這里會自動給角色加上 ROLE_ 前綴
.antMatchers("/user/**").hasRole("user")
//剩下的所有請求,都必須登錄之后才可以訪問
.anyRequest().authenticated()
.and()
.formLogin()
.permitAll()
.and()
.csrf().disable();
return http.build();
}
}2.通過注解來處理角色
1.角色權(quán)限的登錄
step01-設(shè)置角色的注解,在Controller層,方法接口上寫:@PreAuthorize("hasRole('user')"):方法執(zhí)行之前進行校驗有沒有此角色,也可以用過@PreAuthorize("hasAuthortity('teacher:create')")權(quán)限

step02-還要在配置類中,開啟上面的注解的權(quán)限:@EnableGlobalMethodSecurity(prePostEnabled = true)

step03-在service層,在登錄驗證的時候,順便獲取角色查詢,但是在數(shù)據(jù)庫中role字段的值必須有前綴ROLE_

step04-通過User方法里的方法setRoles,將從數(shù)據(jù)庫中用戶roles數(shù)據(jù)存入到內(nèi)存中

step05-然后通過獲取的roles放入到secutity里面,這是從數(shù)據(jù)庫中獲得的角色權(quán)限,這樣就可以聯(lián)想到@PreAuthorize("hasRole('user')")進行對比,這個用戶是否有權(quán)限

2.角色的繼承,層次關(guān)系(Hierarchy)
\r\n可以接著配多個層次關(guān)系

3.自定義權(quán)限匹配方法
1.spel(Spring express language)
@Test
void contextLoads() {
//定義的表達式
String exp1 = "1+2";
//表達式解析器
SpelExpressionParser parser = new SpelExpressionParser();
//解析字符串,將字符串的內(nèi)容當成一個表達式執(zhí)行
Expression expression = parser.parseExpression(exp1);
//獲取執(zhí)行的結(jié)果
Object value = expression.getValue();
System.out.println("value = " + value);
}
@Test
public void test01() {
User user = new User();
user.setUsername("張三");
//這個是計算的上下文對象
StandardEvaluationContext ctx = new StandardEvaluationContext();
ctx.setVariable("user", user);
//這個表達式表示獲取 user 對象中的 username 屬性值
// #user 表示獲取 user 對象
String exp = "#user.username";
SpelExpressionParser parser = new SpelExpressionParser();
Expression expression = parser.parseExpression(exp);
Object value = expression.getValue(ctx);
System.out.println("value = " + value);
}
@Test
public void test02() {
//設(shè)置上下文對象
StandardEvaluationContext ctx = new StandardEvaluationContext();
//獲得User類
User user = new User();
ctx.setVariable("user", user);
//調(diào)用User對象的方法
String exp = "#user.saHello()";
//表達式分析器
SpelExpressionParser parser = new SpelExpressionParser();
//解析字符串
Expression expression = parser.parseExpression(exp);
Object value = expression.getValue(ctx);
System.out.println("value = " + value);
}到此這篇關(guān)于springSecurity使用實戰(zhàn)指南的文章就介紹到這了,更多相關(guān)springSecurity使用內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot整合Hutool實現(xiàn)文件上傳的使用示例
文件上傳在項目經(jīng)常會用到,本文主要介紹了SpringBoot整合Hutool實現(xiàn)文件上傳的使用示例,具有一定的參考價值,感興趣的可以了解一下2023-11-11
Java導出多個excel表打包到zip文件中供客戶端另存為窗口下載實現(xiàn)方法
最近的項目有一個導出匯總數(shù)據(jù)的要求,考慮到用戶軟件的差異,所以要分別導出xls以及xlsx并且打包提供下載,下面這篇文章主要給大家介紹了關(guān)于Java導出多個excel表打包到zip文件中供客戶端另存為窗口下載的實現(xiàn)方法,需要的朋友可以參考下2023-12-12

