SpringSecurity許可權管理系統實戰—七、處理一些問題

codermy發表於2020-08-20

目錄

SpringSecurity許可權管理系統實戰—一、專案簡介和開發環境準備
SpringSecurity許可權管理系統實戰—二、日誌、介面文件等實現
SpringSecurity許可權管理系統實戰—三、主要頁面及介面實現
SpringSecurity許可權管理系統實戰—四、整合SpringSecurity(上)
SpringSecurity許可權管理系統實戰—五、整合SpringSecurity(下)
SpringSecurity許可權管理系統實戰—六、SpringSecurity整合jwt
SpringSecurity許可權管理系統實戰—七、處理一些問題
SpringSecurity許可權管理系統實戰—八、AOP 記錄使用者日誌、異常日誌

前言

在寫完上一篇文章之後,我又研究了很久。最終我發現似乎我們這個專案不太適合用jwt。layui不像vue那樣可以通過axios 全域性設定token(或許是我因為我菜,不知道怎麼設定,如果有小夥伴有好的辦法,歡迎留言告訴我)。
這裡稍微介紹下前端怎麼操作(vue為例),主要就是拿到token以後將其儲存在localstorage或者cookies中,再從localstorage或者cookies中拿到token設定全域性的請求頭,就可以了。
但是前一篇文章關於jwt的內容是沒有問題的,正常的使用也是那樣的步驟。

具體內容

去除JWT

那麼既然不打算再用jwt了,就要老老實實的回去用cookies和session。那麼我們需要把我們的專案還原成實戰五結束時候的樣子。

這裡不是很好解釋了,就是把與jwt相關的刪除就行了,需要修改的地方有MyAuthenticationSuccessHandler,JwtAuthenticationTokenFilter,SpringSecurityConfig三處,刪去有關jwt的內容即可。(不多費篇幅)

前端提示資訊

我這裡修改了一下登入頁面,讓其能夠在登入失敗時,給出提示資訊

<!DOCTYPE html>
<html  xmlns:th="http://www.thymeleaf.org">
	<head>
		<meta charset="utf-8">
		<title></title>
		<link rel="stylesheet" href="/PearAdmin/admin/css/pearForm.css" />
		<link rel="stylesheet" href="/PearAdmin/component/layui/css/layui.css" />
		<link rel="stylesheet" href="/PearAdmin/admin/css/pearButton.css" />
		<link rel="stylesheet" href="/PearAdmin/assets/login.css" />
	</head>
	<body background="PearAdmin/admin/images/background.svg" >
	    <form class="layui-form" method="get">
			<div class="layui-form-item">
				<img class="logo" src="PearAdmin/admin/images/logo.png" />
				<div class="title">M-S-P Admin</div>
				<div class="desc">
					Spring Security 權 限 管 理 系 統 實 戰
				</div>
			</div>
            <div class="layui-form-item">
				<input id="username" name="username" placeholder="用 戶 名 : " type="text" hover class="layui-input" required lay-verify="username"/>
			</div>
			<div class="layui-form-item">
				<input id="password" name="password" placeholder="密 碼 : " type="password"  hover class="layui-input" required lay-verify="password"/>
			</div>
            <div class="layui-form-item">
                <input id="captcha" name="captcha" placeholder="驗 證 碼:" type="text"  hover class="layui-verify" style="border: 1px solid #dcdfe6;" required lay-verify="captcha">
                <img id="captchaImg" src="/captcha" width="130px" height="44px" onclick="this.src=this.src+'?'+Math.random()" title="點選重新整理"/>
            </div>

			<div class="layui-form-item">
				<input type="checkbox" id="rememberme" name="rememberme" title="記住密碼" lay-skin="primary" checked>
			</div>
            <div class="layui-form-item">
				<button style="background-color: #5FB878!important;" class="pear-btn pear-btn-primary login" lay-submit lay-filter="formLogin">
					登 入
				</button>
			</div>
		</form>
		<script src="/PearAdmin/component/layui/layui.js" charset="utf-8"></script>
		<script>
			layui.use(['form', 'element','jquery'], function() {
				var form = layui.form;
				var element = layui.element;
				var $ = layui.jquery;
				// $("body").on("click",".login",function(obj){
				// 	location.href="/api/admin"
				// })
				form.verify({
					username: function(value) {
						if (value.length <= 0 ) {
							return '使用者名稱不能為空';
						}
					},
					password: function (value) {
						if (value.length <= 0) {
							return '密碼不能為空';
						}
					},
					captcha: function (value) {
						if (value.length <= 0) {
							return '驗證碼不能為空';
						}
						if (value.length !== 4) {
							return '請輸入正確格式的驗證碼';
						}
					}
				})
				form.on('submit(formLogin)', function() {
					$.ajax({
						url:'/login',
						type:'post',
						dataType:'text',
						data:{
							username:$('#username').val(),
							password:$('#password').val(),
							captcha:$('#captcha').val(),
							rememberme:$('#rememberme').val()
						},
						success:function(result){
							var restjson = JSON.parse(result)
							if (restjson.success) {
								// layui.data("token", {
								// 	key: "Authorization",
								// 	value: "Bearer "+ restjson.jwt
								// });
								layer.msg(restjson.msg,{icon:1,time:1000},function () {
									location.href = "/";

								});
							}else {
								layer.msg(restjson.msg,{icon:2,time:1000},function () {
									$("#captchaImg").attr("src","/captcha" + "?" + Math.random());
								});
								return false;
							}
						}
					})
					return false;
				});
			})
		</script>
	</body>
</html>

後端也做了登入失敗的處理器

/**
 * @author codermy
 * @createTime 2020/8/2
 */
@Component
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {

    @Override
    public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        httpServletResponse.setCharacterEncoding("utf-8");//修改編碼格式
        httpServletResponse.setContentType("application/json");
        httpServletResponse.getWriter().write(JSON.toJSONString(Result.error().message(e.getMessage())));//返回資訊
    }
}

AuthenticationFailureHandler是一個抽象的異常類,他的常見子類為

UsernameNotFoundException 使用者找不到
BadCredentialsException 壞的憑據
AccountStatusException 使用者狀態異常它包含如下子類
AccountExpiredException 賬戶過期
LockedException 賬戶鎖定
DisabledException 賬戶不可用
CredentialsExpiredException 證照過期

都是在使用者登入時可能會遇到的異常

修改後完整的SpringSecurityConfig

/**
 * @author codermy
 * @createTime 2020/7/15
 */
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private UserDetailsServiceImpl userDetailsService;
    @Autowired
    private VerifyCodeFilter verifyCodeFilter;//驗證碼攔截器
    @Autowired
    MyAuthenticationSuccessHandler authenticationSuccessHandler;//登入成功邏輯
    @Autowired
    private MyAuthenticationFailureHandler authenticationFailureHandler;//登入失敗邏輯
    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;//jwt攔截器
    @Autowired
    private RestAuthenticationEntryPoint restAuthenticationEntryPoint;//無許可權攔截器
    @Autowired
    private RestfulAccessDeniedHandler accessDeniedHandler;// 無權訪問 JSON 格式的資料



    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring()
                .antMatchers(HttpMethod.GET,
                        "/swagger-resources/**",
                        "/PearAdmin/**",
                        "/**/*.html",
                        "/**/*.css",
                        "/**/*.js",
                        "/swagger-ui.html",
                        "/webjars/**",
                        "/v2/**");//放行靜態資源
    }

    /**
     * anyRequest          |   匹配所有請求路徑
     * access              |   SpringEl表示式結果為true時可以訪問
     * anonymous           |   匿名可以訪問
     * denyAll             |   使用者不能訪問
     * fullyAuthenticated  |   使用者完全認證可以訪問(非remember-me下自動登入)
     * hasAnyAuthority     |   如果有引數,參數列示許可權,則其中任何一個許可權可以訪問
     * hasAnyRole          |   如果有引數,參數列示角色,則其中任何一個角色可以訪問
     * hasAuthority        |   如果有引數,參數列示許可權,則其許可權可以訪問
     * hasIpAddress        |   如果有引數,參數列示IP地址,如果使用者IP和引數匹配,則可以訪問
     * hasRole             |   如果有引數,參數列示角色,則其角色可以訪問
     * permitAll           |   使用者可以任意訪問
     * rememberMe          |   允許通過remember-me登入的使用者訪問
     * authenticated       |   使用者登入後可訪問
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.addFilterBefore(verifyCodeFilter, UsernamePasswordAuthenticationFilter.class);
        http.csrf().disable()//關閉csrf
                // .sessionManagement()// 基於token,所以不需要session
                // .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                // .and()
                .httpBasic().authenticationEntryPoint(restAuthenticationEntryPoint)//未登陸時返回 JSON 格式的資料給前端
                .and()
                .authorizeRequests()
                .antMatchers("/captcha").permitAll()//任何人都能訪問這個請求
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/login.html")//登入頁面 不設限訪問
                .loginProcessingUrl("/login")//攔截的請求
                .successHandler(authenticationSuccessHandler) // 登入成功
                .failureHandler(authenticationFailureHandler) // 登入失敗
                .permitAll()
                .and()
                .rememberMe().rememberMeParameter("rememberme")
                // 防止iframe 造成跨域
                .and()
                .headers()
                .frameOptions()
                .disable()
                .and();

        // 禁用快取
        http.headers().cacheControl();

        // 新增JWT攔截器
        // http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        http.exceptionHandling().accessDeniedHandler(accessDeniedHandler); // 無權訪問返回JSON 格式的資料
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(12);
    }

    /**
     * 身份認證介面
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }


}

遇到的問題

一、Springsecurity中的UsernameNotFoundException異常無法被正常捕獲

具體的解釋可以看這篇文章(非常詳細,包括解決方案)

簡而言之,就是我丟擲了UsernameNotFoundException異常但是最後會被轉換為BadCredentialsException異常。我這裡不多做介紹了,上面那篇文章說的非常詳細。
在這裡插入圖片描述

如何解決也請參照那篇文章。我所使用的是取巧的方法,就是直接丟擲BadCredentialsException異常而不是UsernameNotFoundException異常。因為畢竟最後給出的提示資訊是模糊的“使用者名稱或密碼錯誤”,而不是具體到哪個錯誤了。

二、無法統一處理filter中丟擲的異常

這個問題主要是和驗證碼的攔截器有關,前端拿不到驗證碼錯誤的提示資訊。這裡我們可以不用攔截器來處理驗證碼,可以自定義一個login請求來避開這個問題。

這個問題也是原本的寫法問題吧,其實原本需要用拋這個異常,直接向頁面輸出提示資訊就好了。

我在找處理方法時找到有兩種方法供大家參考

後敘

這篇文章有點亂,博主的文筆真的不太行,所以在描述一些問題的時候可能會有點難以理解。如果小夥伴們在學習過程中有什麼問題,歡迎大家加我的qq(在我的碼雲主頁有)我們一起探討學習。
下一篇文章我們實現使用者的操作日誌和異常日誌功能

giteegithub中可獲取原始碼,與本系列文章同步更新

相關文章