Spring Security 自定義登錄認(rèn)證(二)

一口芍、前言

本篇文章將講述Spring Security自定義登錄認(rèn)證校驗(yàn)用戶名、密碼驻右,自定義密碼加密方式损搬,以及在前后端分離的情況下認(rèn)證失敗或成功處理返回json格式數(shù)據(jù)

溫馨小提示: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)登錄訪問時(shí)給它賦予最高權(quán)限,可以訪問系統(tǒng)所有api接口票顾,或在登錄認(rèn)證成功后存入token以便用戶訪問系統(tǒng)其它接口時(shí)通過token認(rèn)證用戶權(quán)限等

Spring Security入門學(xué)習(xí)可參考之前文章:

SpringBoot集成Spring Security入門體驗(yàn)(一)

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

二础浮、Spring Security 自定義登錄認(rèn)證處理

基本環(huán)境
  1. spring-boot 2.1.8
  2. mybatis-plus 2.2.0
  3. mysql
  4. maven項(xiàng)目

數(shù)據(jù)庫(kù)用戶信息表t_sys_user

在這里插入圖片描述

案例中關(guān)于對(duì)該t_sys_user用戶表相關(guān)的增刪改查代碼就不貼出來了,如有需要可參考文末提供的案例demo源碼

1奠骄、Security 核心配置類

配置用戶密碼校驗(yàn)過濾器

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

    /**
     * 用戶密碼校驗(yàn)過濾器
     */
    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<HttpSecurity>.ExpressionInterceptUrlRegistry registry = http.antMatcher("/**").authorizeRequests();

        // 禁用CSRF 開啟跨域
        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] 訪問`/home`接口,其他ip地址無法訪問
        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)作情況下影锈, 允許客戶端來確定與資源相關(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();

        // 自定義過濾器認(rèn)證用戶名密碼
        http.addFilterAt(adminAuthenticationProcessingFilter, UsernamePasswordAuthenticationFilter.class);
    }
}

2、自定義用戶密碼校驗(yàn)過濾器

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

3鸭廷、自定義認(rèn)證管理器

@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枣抱、自定義認(rèn)證處理

這里的密碼加密驗(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ò)誤辆床!");
        }

        // 前后端分離情況下 處理邏輯...
        // 更新登錄令牌 - 之后訪問系統(tǒng)其它接口直接通過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<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實(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<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、自定義認(rèn)證成功或失敗處理方式

  1. 認(rèn)證成功處理類實(shí)現(xiàn)AuthenticationSuccessHandler類重寫onAuthenticationSuccess方法
  2. 認(rèn)證失敗處理類實(shí)現(xiàn)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("證書過期,請(qǐng)聯(lián)系管理員!");
        } else if (e instanceof AccountExpiredException) {
            result = ApiResult.fail("賬戶過期维雇,請(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);
    }
}
溫馨小提示:

前后端一體的情況下可通過在Spring Security核心配置類中配置異常處理接口然后通過如下方式獲取異常信息

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

三吱型、前端頁(yè)面

這里2個(gè)簡(jiǎn)單的html頁(yè)面模擬前后端分離情況下登陸處理場(chǎng)景

1逸贾、登陸頁(yè)

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"/>自動(dòng)登錄</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、首頁(yè)

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>

四铝侵、測(cè)試接口

@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`權(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)限訪問接口";
    }

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

五、測(cè)試訪問效果

數(shù)據(jù)庫(kù)賬號(hào):admin 密碼:123456

1. 輸入錯(cuò)誤用戶名提示該用戶不存在
在這里插入圖片描述
2. 輸入錯(cuò)誤密碼提示密碼錯(cuò)誤
在這里插入圖片描述
3. 輸入正確用戶名和賬號(hào)触徐,提示登陸成功咪鲜,然后跳轉(zhuǎn)到首頁(yè)
在這里插入圖片描述

在這里插入圖片描述

登陸成功后即可正常訪問其他接口,如果是未登錄情況下將訪問不了

在這里插入圖片描述

溫馨小提示:這里在未登錄時(shí)或訪問未授權(quán)的接口時(shí)撞鹉,后端暫時(shí)沒有做處理疟丙,相關(guān)案例將會(huì)放在后面的權(quán)限控制案例教程中講解

在這里插入圖片描述

六、總結(jié)

  1. Spring Security核心配置類中設(shè)置自定義的用戶密碼校驗(yàn)過濾器(AdminAuthenticationProcessingFilter)
  2. 在自定義的用戶密碼校驗(yàn)過濾器中配置認(rèn)證管理器(CusAuthenticationManager)鸟雏、認(rèn)證成功處理(AdminAuthenticationSuccessHandler)認(rèn)證失敗處理(AdminAuthenticationFailureHandler)
  3. 在自定義的認(rèn)證管理器中配置自定義的認(rèn)證處理(AdminAuthenticationProvider)
  4. 然后就是在認(rèn)證處理中實(shí)現(xiàn)自己的相應(yīng)業(yè)務(wù)邏輯等
Security相關(guān)代碼結(jié)構(gòu):
在這里插入圖片描述

本文案例源碼

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末享郊,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子孝鹊,更是在濱河造成了極大的恐慌炊琉,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,607評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件又活,死亡現(xiàn)場(chǎng)離奇詭異苔咪,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)皇钞,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,239評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門悼泌,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人夹界,你說我怎么就攤上這事馆里“溃” “怎么了?”我有些...
    開封第一講書人閱讀 164,960評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵鸠踪,是天一觀的道長(zhǎng)丙者。 經(jīng)常有香客問我,道長(zhǎng)营密,這世上最難降的妖魔是什么械媒? 我笑而不...
    開封第一講書人閱讀 58,750評(píng)論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮评汰,結(jié)果婚禮上纷捞,老公的妹妹穿的比我還像新娘。我一直安慰自己被去,他們只是感情好主儡,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,764評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著惨缆,像睡著了一般糜值。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上坯墨,一...
    開封第一講書人閱讀 51,604評(píng)論 1 305
  • 那天寂汇,我揣著相機(jī)與錄音,去河邊找鬼捣染。 笑死骄瓣,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的液斜。 我是一名探鬼主播累贤,決...
    沈念sama閱讀 40,347評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼少漆!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起硼被,我...
    開封第一講書人閱讀 39,253評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤示损,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后嚷硫,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體检访,經(jīng)...
    沈念sama閱讀 45,702評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,893評(píng)論 3 336
  • 正文 我和宋清朗相戀三年仔掸,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了脆贵。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,015評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡起暮,死狀恐怖卖氨,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤筒捺,帶...
    沈念sama閱讀 35,734評(píng)論 5 346
  • 正文 年R本政府宣布柏腻,位于F島的核電站,受9級(jí)特大地震影響系吭,放射性物質(zhì)發(fā)生泄漏五嫂。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,352評(píng)論 3 330
  • 文/蒙蒙 一肯尺、第九天 我趴在偏房一處隱蔽的房頂上張望沃缘。 院中可真熱鬧,春花似錦则吟、人聲如沸槐臀。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,934評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽峰档。三九已至,卻和暖如春寨昙,著一層夾襖步出監(jiān)牢的瞬間讥巡,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,052評(píng)論 1 270
  • 我被黑心中介騙來泰國(guó)打工舔哪, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留欢顷,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,216評(píng)論 3 371
  • 正文 我出身青樓捉蚤,卻偏偏與公主長(zhǎng)得像抬驴,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子缆巧,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,969評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容