1. 程式人生 > >Spring Security 自定義登入認證(二)

Spring Security 自定義登入認證(二)

一、前言

本篇文章將講述Spring Security自定義登入認證校驗使用者名稱、密碼,自定義密碼加密方式,以及在前後端分離的情況下認證失敗或成功處理返回json格式資料

溫馨小提示:Spring Security中有預設的密碼加密方式以及登入使用者認證校驗,但小編這裡選擇自定義是為了方便以後業務擴充套件,比如系統預設帶一個超級管理員,當認證時識別到是超級管理員賬號登入訪問時給它賦予最高許可權,可以訪問系統所有api介面,或在登入認證成功後存入token以便使用者訪問系統其它介面時通過token認證使用者許可權等

Spring Security入門學習可參考之前文章:

SpringBoot整合Spring Security入門體驗(一)

https://blog.csdn.net/qq_38225558/article/details/101754743

二、Spring Security 自定義登入認證處理

基本環境
  1. spring-boot 2.1.8
  2. mybatis-plus 2.2.0
  3. mysql
  4. maven專案

資料庫使用者資訊表t_sys_user

案例中關於對該t_sys_user使用者表相關的增刪改查程式碼就不貼出來了,如有需要可參考文末提供的案例demo原始碼

1、Security 核心配置類

配置使用者密碼校驗過濾器

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 使用者密碼校驗過濾器
     */
    private final AdminAuthenticationProcessingFilter adminAuthenticationProcessingFilter;

    public SecurityConfig(AdminAuthenticationProcessingFilter adminAuthenticationProcessingFilter) {
        this.adminAuthenticationProcessingFilter = adminAuthenticationProcessingFilter;
    }

    /**
     * 許可權配置
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = http.antMatcher("/**").authorizeRequests();

        // 禁用CSRF 開啟跨域
        http.csrf().disable().cors();

        // 登入處理 - 前後端一體的情況下
//        registry.and().formLogin().loginPage("/login").defaultSuccessUrl("/").permitAll()
//                // 自定義登陸使用者名稱和密碼屬性名,預設為 username和password
//                .usernameParameter("username").passwordParameter("password")
//                // 異常處理
//                .failureUrl("/login/error").permitAll()
//                // 退出登入
//                .and().logout().permitAll();

        // 標識只能在 伺服器本地ip[127.0.0.1或localhost] 訪問`/home`介面,其他ip地址無法訪問
        registry.antMatchers("/home").hasIpAddress("127.0.0.1");
        // 允許匿名的url - 可理解為放行介面 - 多個介面使用,分割
        registry.antMatchers("/login", "/index").permitAll();
        // OPTIONS(選項):查詢適用於一個特定網址資源的通訊選擇。 在不需執行具體的涉及資料傳輸的動作情況下, 允許客戶端來確定與資源相關的選項以及 / 或者要求, 或是一個伺服器的效能
        registry.antMatchers(HttpMethod.OPTIONS, "/**").denyAll();
        // 自動登入 - cookie儲存方式
        registry.and().rememberMe();
        // 其餘所有請求都需要認證
        registry.anyRequest().authenticated();
        // 防止iframe 造成跨域
        registry.and().headers().frameOptions().disable();

        // 自定義過濾器認證使用者名稱密碼
        http.addFilterAt(adminAuthenticationProcessingFilter, UsernamePasswordAuthenticationFilter.class);
    }
}

2、自定義使用者密碼校驗過濾器

@Slf4j
@Component
public class AdminAuthenticationProcessingFilter extends AbstractAuthenticationProcessingFilter {

    /**
     * @param authenticationManager:             認證管理器
     * @param adminAuthenticationSuccessHandler: 認證成功處理
     * @param adminAuthenticationFailureHandler: 認證失敗處理
     */
    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("請求頭型別不支援: " + request.getContentType());
        }

        UsernamePasswordAuthenticationToken authRequest;
        try {
            MultiReadHttpServletRequest wrappedRequest = new MultiReadHttpServletRequest(request);
            // 將前端傳遞的資料轉換成jsonBean資料格式
            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);
    }
}

3、自定義認證管理器

@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!");
    }
}

4、自定義認證處理

這裡的密碼加密驗證工具類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());
        // 驗證密碼
        if (!isValid) {
            throw new BadCredentialsException("密碼錯誤!");
        }

        // 前後端分離情況下 處理邏輯...
        // 更新登入令牌 - 之後訪問系統其它介面直接通過token認證使用者許可權...
        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;
    }
}

其中小編自定義了一個UserDetailsServiceImpl類去實現UserDetailsService類 -> 用於認證使用者詳情
和自定義一個SecurityUser類實現UserDetails類 -> 安全認證使用者詳情資訊

@Service("userDetailsService")
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    /***
     * 根據賬號獲取使用者資訊
     * @param username:
     * @return: org.springframework.security.core.userdetails.UserDetails
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 從資料庫中取出使用者資訊
        List<User> userList = userMapper.selectList(new EntityWrapper<User>().eq("username", username));
        User user;
        // 判斷使用者是否存在
        if (!CollectionUtils.isEmpty(userList)){
            user = userList.get(0);
        } else {
            throw new UsernameNotFoundException("使用者名稱不存在!");
        }
        // 返回UserDetails實現類
        return new SecurityUser(user);
    }
}

安全認證使用者詳情資訊

@Data
@Slf4j
public class SecurityUser implements UserDetails {
    /**
     * 當前登入使用者
     */
    private transient User currentUserInfo;

    public SecurityUser() {
    }

    public SecurityUser(User user) {
        if (user != null) {
            this.currentUserInfo = user;
        }
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> 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;
    }
}

5、自定義認證成功或失敗處理方式

  1. 認證成功處理類實現AuthenticationSuccessHandler類重寫onAuthenticationSuccess方法
  2. 認證失敗處理類實現AuthenticationFailureHandler類重寫onAuthenticationFailure方法

在前後端分離情況下小編認證成功和失敗都返回json資料格式

認證成功後這裡小編只返回了一個token給前端,其它資訊可根據個人業務實際處理

@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));
    }
}

認證失敗捕捉異常自定義錯誤資訊返回給前端

@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("賬戶被鎖定,請聯絡管理員!");
        } else if (e instanceof CredentialsExpiredException) {
            result = ApiResult.fail("證書過期,請聯絡管理員!");
        } else if (e instanceof AccountExpiredException) {
            result = ApiResult.fail("賬戶過期,請聯絡管理員!");
        } else if (e instanceof DisabledException) {
            result = ApiResult.fail("賬戶被禁用,請聯絡管理員!");
        } else {
            log.error("登入失敗:", e);
            result = ApiResult.fail("登入失敗!");
        }
        ResponseUtils.out(response, result);
    }
}
溫馨小提示:

前後端一體的情況下可通過在Spring Security核心配置類中配置異常處理介面然後通過如下方式獲取異常資訊

AuthenticationException e = (AuthenticationException) request.getSession().getAttribute("SPRING_SECURITY_LAST_EXCEPTION");
System.out.println(e.getMessage());

三、前端頁面

這裡2個簡單的html頁面模擬前後端分離情況下登陸處理場景

1、登陸頁

login.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Login</title>
</head>
<body>
<h1>Spring Security</h1>
<form method="post" action="" onsubmit="return false">
    <div>
        使用者名稱:<input type="text" name="username" id="username">
    </div>
    <div>
        密碼:<input type="password" name="password" id="password">
    </div>
    <div>
<!--        <label><input type="checkbox" name="remember-me" id="remember-me"/>自動登入</label>-->
        <button onclick="login()">登陸</button>
    </div>
</form>
</body>
<script src="http://libs.baidu.com/jquery/1.9.0/jquery.js" type="text/javascript"></script>
<script type="text/javascript">
    function login() {
        var username = document.getElementById("username").value;
        var password = document.getElementById("password").value;
        // var rememberMe = document.getElementById("remember-me").value;
        $.ajax({
            async: false,
            type: "POST",
            dataType: "json",
            url: '/login',
            contentType: "application/json",
            data: JSON.stringify({
                "username": username,
                "password": password
                // "remember-me": rememberMe
            }),
            success: function (result) {
                console.log(result)
                if (result.code == 200) {
                    alert("登陸成功");
                    window.location.href = "../home.html";
                } else {
                    alert(result.message)
                }
            }
        });
    }
</script>
</html>
2、首頁

home.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h3>您好,登陸成功</h3>
<button onclick="window.location.href='/logout'">退出登入</button>
</body>
</html>

四、測試介面

@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")
    // 訪問路徑`/admin` 具有`crud`許可權
    @PreAuthorize("hasPermission('/admin','crud')")
    public String admin() {
        return "Hello~ 管理員";
    }

    @GetMapping("/test")
//    @PreAuthorize("hasPermission('/test','t')")
    public String test() {
        return "Hello~ 測試許可權訪問介面";
    }

    /**
     * 登入異常處理 - 前後端一體的情況下
     * @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()));
    }
}

五、測試訪問效果

資料庫賬號:admin 密碼:123456

1. 輸入錯誤使用者名稱提示該使用者不存在

2. 輸入錯誤密碼提示密碼錯誤

3. 輸入正確使用者名稱和賬號,提示登陸成功,然後跳轉到首頁


登陸成功後即可正常訪問其他介面,如果是未登入情況下將訪問不了

溫馨小提示:這裡在未登入時或訪問未授權的介面時,後端暫時沒有做處理,相關案例將會放在後面的許可權控制案例教程中講解

六、總結

  1. Spring Security核心配置類中設定自定義的使用者密碼校驗過濾器(AdminAuthenticationProcessingFilter)
  2. 在自定義的使用者密碼校驗過濾器中配置認證管理器(CusAuthenticationManager)認證成功處理(AdminAuthenticationSuccessHandler)認證失敗處理(AdminAuthenticationFailureHandler)
  3. 在自定義的認證管理器中配置自定義的認證處理(AdminAuthenticationProvider)
  4. 然後就是在認證處理中實現自己的相應業務邏輯等
Security相關程式碼結構:

本文案例原始碼

https://gitee.com/zhengqingya/java-worksp