本篇文章將講述Spring Security自定義登錄認(rèn)證校驗(yàn)用戶名、密碼,自定義密碼加密方式,以及在前后端分離的情況下認(rèn)證失敗或成功處理返回json格式數(shù)據(jù)
成都創(chuàng)新互聯(lián)始終致力于在企業(yè)網(wǎng)站建設(shè)領(lǐng)域發(fā)展。秉承“創(chuàng)新、求實(shí)、誠(chéng)信、拼搏”的企業(yè)精神,致力為企業(yè)提供全面的網(wǎng)絡(luò)宣傳與技術(shù)應(yīng)用整體策劃方案,為企業(yè)提供包括“網(wǎng)站建設(shè)、成都響應(yīng)式網(wǎng)站建設(shè)、手機(jī)網(wǎng)站建設(shè)、微信網(wǎng)站建設(shè)、小程序設(shè)計(jì)、商城建設(shè)、平臺(tái)網(wǎng)站建設(shè)秉承"和諧、參與、激情"的文化,與客戶和合作伙伴齊心協(xié)力一起成長(zhǎng),共同發(fā)展。
溫馨小提示:Spring Security中有默認(rèn)的密碼加密方式以及登錄用戶認(rèn)證校驗(yàn),但小編這里選擇自定義是為了方便以后業(yè)務(wù)擴(kuò)展,比如系統(tǒng)默認(rèn)帶一個(gè)超級(jí)管理員,當(dāng)認(rèn)證時(shí)識(shí)別到是超級(jí)管理員賬號(hào)登錄訪問(wèn)時(shí)給它賦予最高權(quán)限,可以訪問(wèn)系統(tǒng)所有api接口,或在登錄認(rèn)證成功后存入token以便用戶訪問(wèn)系統(tǒng)其它接口時(shí)通過(guò)token認(rèn)證用戶權(quán)限等
SpringBoot集成Spring Security入門體驗(yàn)(一)
https://blog.csdn.net/qq_38225558/article/details/101754743
數(shù)據(jù)庫(kù)用戶信息表t_sys_user
案例中關(guān)于對(duì)該
t_sys_user
用戶表相關(guān)的增刪改查代碼就不貼出來(lái)了,如有需要可參考文末提供的案例demo源碼
配置用戶密碼校驗(yàn)過(guò)濾器
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 用戶密碼校驗(yàn)過(guò)濾器
*/
private final AdminAuthenticationProcessingFilter adminAuthenticationProcessingFilter;
public SecurityConfig(AdminAuthenticationProcessingFilter adminAuthenticationProcessingFilter) {
this.adminAuthenticationProcessingFilter = adminAuthenticationProcessingFilter;
}
/**
* 權(quán)限配置
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry registry = http.antMatcher("/**").authorizeRequests();
// 禁用CSRF 開(kāi)啟跨域
http.csrf().disable().cors();
// 登錄處理 - 前后端一體的情況下
// registry.and().formLogin().loginPage("/login").defaultSuccessUrl("/").permitAll()
// // 自定義登陸用戶名和密碼屬性名,默認(rèn)為 username和password
// .usernameParameter("username").passwordParameter("password")
// // 異常處理
// .failureUrl("/login/error").permitAll()
// // 退出登錄
// .and().logout().permitAll();
// 標(biāo)識(shí)只能在 服務(wù)器本地ip[127.0.0.1或localhost] 訪問(wèn)`/home`接口,其他ip地址無(wú)法訪問(wèn)
registry.antMatchers("/home").hasIpAddress("127.0.0.1");
// 允許匿名的url - 可理解為放行接口 - 多個(gè)接口使用,分割
registry.antMatchers("/login", "/index").permitAll();
// OPTIONS(選項(xiàng)):查找適用于一個(gè)特定網(wǎng)址資源的通訊選擇。 在不需執(zhí)行具體的涉及數(shù)據(jù)傳輸?shù)膭?dòng)作情況下, 允許客戶端來(lái)確定與資源相關(guān)的選項(xiàng)以及 / 或者要求, 或是一個(gè)服務(wù)器的性能
registry.antMatchers(HttpMethod.OPTIONS, "/**").denyAll();
// 自動(dòng)登錄 - cookie儲(chǔ)存方式
registry.and().rememberMe();
// 其余所有請(qǐng)求都需要認(rèn)證
registry.anyRequest().authenticated();
// 防止iframe 造成跨域
registry.and().headers().frameOptions().disable();
// 自定義過(guò)濾器認(rèn)證用戶名密碼
http.addFilterAt(adminAuthenticationProcessingFilter, UsernamePasswordAuthenticationFilter.class);
}
}
@Slf4j
@Component
public class AdminAuthenticationProcessingFilter extends AbstractAuthenticationProcessingFilter {
/**
* @param authenticationManager: 認(rèn)證管理器
* @param adminAuthenticationSuccessHandler: 認(rèn)證成功處理
* @param adminAuthenticationFailureHandler: 認(rèn)證失敗處理
*/
public AdminAuthenticationProcessingFilter(CusAuthenticationManager authenticationManager, AdminAuthenticationSuccessHandler adminAuthenticationSuccessHandler, AdminAuthenticationFailureHandler adminAuthenticationFailureHandler) {
super(new AntPathRequestMatcher("/login", "POST"));
this.setAuthenticationManager(authenticationManager);
this.setAuthenticationSuccessHandler(adminAuthenticationSuccessHandler);
this.setAuthenticationFailureHandler(adminAuthenticationFailureHandler);
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (request.getContentType() == null || !request.getContentType().contains(Constants.REQUEST_HEADERS_CONTENT_TYPE)) {
throw new AuthenticationServiceException("請(qǐng)求頭類型不支持: " + request.getContentType());
}
UsernamePasswordAuthenticationToken authRequest;
try {
MultiReadHttpServletRequest wrappedRequest = new MultiReadHttpServletRequest(request);
// 將前端傳遞的數(shù)據(jù)轉(zhuǎn)換成jsonBean數(shù)據(jù)格式
User user = JSONObject.parseObject(wrappedRequest.getBodyJsonStrByJson(wrappedRequest), User.class);
authRequest = new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword(), null);
authRequest.setDetails(authenticationDetailsSource.buildDetails(wrappedRequest));
} catch (Exception e) {
throw new AuthenticationServiceException(e.getMessage());
}
return this.getAuthenticationManager().authenticate(authRequest);
}
}
@Component
public class CusAuthenticationManager implements AuthenticationManager {
private final AdminAuthenticationProvider adminAuthenticationProvider;
public CusAuthenticationManager(AdminAuthenticationProvider adminAuthenticationProvider) {
this.adminAuthenticationProvider = adminAuthenticationProvider;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Authentication result = adminAuthenticationProvider.authenticate(authentication);
if (Objects.nonNull(result)) {
return result;
}
throw new ProviderNotFoundException("Authentication failed!");
}
}
這里的密碼加密驗(yàn)證工具類PasswordUtils
可在文末源碼中查看
@Component
public class AdminAuthenticationProvider implements AuthenticationProvider {
@Autowired
UserDetailsServiceImpl userDetailsService;
@Autowired
private UserMapper userMapper;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// 獲取前端表單中輸入后返回的用戶名、密碼
String userName = (String) authentication.getPrincipal();
String password = (String) authentication.getCredentials();
SecurityUser userInfo = (SecurityUser) userDetailsService.loadUserByUsername(userName);
boolean isValid = PasswordUtils.isValidPassword(password, userInfo.getPassword(), userInfo.getCurrentUserInfo().getSalt());
// 驗(yàn)證密碼
if (!isValid) {
throw new BadCredentialsException("密碼錯(cuò)誤!");
}
// 前后端分離情況下 處理邏輯...
// 更新登錄令牌 - 之后訪問(wèn)系統(tǒng)其它接口直接通過(guò)token認(rèn)證用戶權(quán)限...
String token = PasswordUtils.encodePassword(System.currentTimeMillis() + userInfo.getCurrentUserInfo().getSalt(), userInfo.getCurrentUserInfo().getSalt());
User user = userMapper.selectById(userInfo.getCurrentUserInfo().getId());
user.setToken(token);
userMapper.updateById(user);
userInfo.getCurrentUserInfo().setToken(token);
return new UsernamePasswordAuthenticationToken(userInfo, password, userInfo.getAuthorities());
}
@Override
public boolean supports(Class> aClass) {
return true;
}
}
其中小編自定義了一個(gè)UserDetailsServiceImpl
類去實(shí)現(xiàn)UserDetailsService
類 -> 用于認(rèn)證用戶詳情
和自定義一個(gè)SecurityUser
類實(shí)現(xiàn)UserDetails
類 -> 安全認(rèn)證用戶詳情信息
@Service("userDetailsService")
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
/***
* 根據(jù)賬號(hào)獲取用戶信息
* @param username:
* @return: org.springframework.security.core.userdetails.UserDetails
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 從數(shù)據(jù)庫(kù)中取出用戶信息
List userList = userMapper.selectList(new EntityWrapper().eq("username", username));
User user;
// 判斷用戶是否存在
if (!CollectionUtils.isEmpty(userList)){
user = userList.get(0);
} else {
throw new UsernameNotFoundException("用戶名不存在!");
}
// 返回UserDetails實(shí)現(xiàn)類
return new SecurityUser(user);
}
}
安全認(rèn)證用戶詳情信息
@Data
@Slf4j
public class SecurityUser implements UserDetails {
/**
* 當(dāng)前登錄用戶
*/
private transient User currentUserInfo;
public SecurityUser() {
}
public SecurityUser(User user) {
if (user != null) {
this.currentUserInfo = user;
}
}
@Override
public Collection extends GrantedAuthority> getAuthorities() {
Collection authorities = new ArrayList<>();
SimpleGrantedAuthority authority = new SimpleGrantedAuthority("admin");
authorities.add(authority);
return authorities;
}
@Override
public String getPassword() {
return currentUserInfo.getPassword();
}
@Override
public String getUsername() {
return currentUserInfo.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;
}
}
AuthenticationSuccessHandler
類重寫onAuthenticationSuccess
方法AuthenticationFailureHandler
類重寫onAuthenticationFailure
方法在前后端分離情況下小編認(rèn)證成功和失敗都返回json數(shù)據(jù)格式
認(rèn)證成功后這里小編只返回了一個(gè)token給前端,其它信息可根據(jù)個(gè)人業(yè)務(wù)實(shí)際處理
@Component
public class AdminAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse response, Authentication auth) throws IOException, ServletException {
User user = new User();
SecurityUser securityUser = ((SecurityUser) auth.getPrincipal());
user.setToken(securityUser.getCurrentUserInfo().getToken());
ResponseUtils.out(response, ApiResult.ok("登錄成功!", user));
}
}
認(rèn)證失敗捕捉異常自定義錯(cuò)誤信息返回給前端
@Slf4j
@Component
public class AdminAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
ApiResult result;
if (e instanceof UsernameNotFoundException || e instanceof BadCredentialsException) {
result = ApiResult.fail(e.getMessage());
} else if (e instanceof LockedException) {
result = ApiResult.fail("賬戶被鎖定,請(qǐng)聯(lián)系管理員!");
} else if (e instanceof CredentialsExpiredException) {
result = ApiResult.fail("證書(shū)過(guò)期,請(qǐng)聯(lián)系管理員!");
} else if (e instanceof AccountExpiredException) {
result = ApiResult.fail("賬戶過(guò)期,請(qǐng)聯(lián)系管理員!");
} else if (e instanceof DisabledException) {
result = ApiResult.fail("賬戶被禁用,請(qǐng)聯(lián)系管理員!");
} else {
log.error("登錄失?。?, e);
result = ApiResult.fail("登錄失敗!");
}
ResponseUtils.out(response, result);
}
}
前后端一體的情況下可通過(guò)在Spring Security核心配置類
中配置異常處理接口然后通過(guò)如下方式獲取異常信息
AuthenticationException e = (AuthenticationException) request.getSession().getAttribute("SPRING_SECURITY_LAST_EXCEPTION");
System.out.println(e.getMessage());
這里2個(gè)簡(jiǎn)單的html頁(yè)面模擬前后端分離情況下登陸處理場(chǎng)景
login.html
Login
Spring Security
home.html
Title
您好,登陸成功
@Slf4j
@RestController
public class IndexController {
@GetMapping("/")
public ModelAndView showHome() {
return new ModelAndView("home.html");
}
@GetMapping("/index")
public String index() {
return "Hello World ~";
}
@GetMapping("/login")
public ModelAndView login() {
return new ModelAndView("login.html");
}
@GetMapping("/home")
public String home() {
String name = SecurityContextHolder.getContext().getAuthentication().getName();
log.info("登陸人:" + name);
return "Hello~ " + name;
}
@GetMapping(value ="/admin")
// 訪問(wèn)路徑`/admin` 具有`crud`權(quán)限
@PreAuthorize("hasPermission('/admin','crud')")
public String admin() {
return "Hello~ 管理員";
}
@GetMapping("/test")
// @PreAuthorize("hasPermission('/test','t')")
public String test() {
return "Hello~ 測(cè)試權(quán)限訪問(wèn)接口";
}
/**
* 登錄異常處理 - 前后端一體的情況下
* @param request
* @param response
*/
@RequestMapping("/login/error")
public void loginError(HttpServletRequest request, HttpServletResponse response) {
AuthenticationException e = (AuthenticationException) request.getSession().getAttribute("SPRING_SECURITY_LAST_EXCEPTION");
log.error(e.getMessage());
ResponseUtils.out(response, ApiResult.fail(e.getMessage()));
}
}
數(shù)據(jù)庫(kù)賬號(hào):admin 密碼:123456
登陸成功后即可正常訪問(wèn)其他接口,如果是未登錄情況下將訪問(wèn)不了
溫馨小提示:這里在未登錄時(shí)或訪問(wèn)未授權(quán)的接口時(shí),后端暫時(shí)沒(méi)有做處理,相關(guān)案例將會(huì)放在后面的權(quán)限控制案例教程中講解
Spring Security核心配置類
中設(shè)置自定義的用戶密碼校驗(yàn)過(guò)濾器(AdminAuthenticationProcessingFilter)
認(rèn)證管理器(CusAuthenticationManager)
、認(rèn)證成功處理(AdminAuthenticationSuccessHandler)
和認(rèn)證失敗處理(AdminAuthenticationFailureHandler)
等認(rèn)證處理(AdminAuthenticationProvider)
https://gitee.com/zhengqingya/java-workspace