使用SpringBoot Security進(jìn)行登錄驗(yàn)證棺榔,可以結(jié)合具體的業(yè)務(wù)需求來使用。在
SpringBoot Security前后端分離隘道,登錄退出等返回json
一文中症歇,描述了前后端分離的情況下,如何進(jìn)行登錄驗(yàn)證和提示錯誤信息的√饭#現(xiàn)在針對自定義的登錄頁面忘晤,能夠精確地提示錯誤信息,做一個簡單的演示demo激捏。
本文使用的SpringBoot版本是2.1.4.RELEASE设塔,下面直接進(jìn)入使用階段。
第一步远舅,在pom.xml中引入架包
<!-- security架包 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
加上這個架包壹置,重啟項(xiàng)目后,整個項(xiàng)目就配置了登錄攔截和驗(yàn)證表谊。
第二步钞护,重啟項(xiàng)目,會在控制臺下看到自動生成的登錄密碼爆办,默認(rèn)的用戶名是admin
第三步难咕,打開瀏覽器窗口,對登錄頁面進(jìn)行探究
不輸入用戶名和密碼,直接點(diǎn)擊登錄時余佃,會有提示信息暮刃,輸入框的顏色還會變紅。查看源碼爆土,可以發(fā)現(xiàn)椭懊,架包默認(rèn)的登錄頁面提交方式為表單提交,method為post步势,并且默認(rèn)是開啟csrf的氧猬,在表單里自動生成了一個隱藏域,防止跨域提交坏瘩,確保請求的安全性盅抚。
輸入錯誤的用戶名或密碼,可以看到頁面進(jìn)行了跳轉(zhuǎn)倔矾,跳轉(zhuǎn)后的頁面又回到了登錄頁妄均,只是url地址后面多了一個參數(shù),頁面提示錯誤信息哪自。
從頁面源碼丰包,我們可以獲得以下幾個方面的信息:
- 自帶的登錄頁面使用post方式提交;
- 用戶名密碼錯誤時壤巷,頁面會進(jìn)行重定向邑彪,重定向到登錄頁面,并展示錯誤信息隙笆。
如果頁面是我們自己自定義的锌蓄,如果要使用默認(rèn)的過濾器獲取登錄信息,則必須使用post方式進(jìn)行提交撑柔,如果使用ajax json的方式進(jìn)行提交瘸爽,則獲取不到參數(shù)。
接下來自定義一個登錄頁面铅忿,為了快速構(gòu)建登錄頁面剪决,這里使用了thymeleaf模板。
第一步檀训,在配置文件中柑潦,引入thymeleaf架包
<!-- 導(dǎo)入Spring Boot的thymeleaf依賴-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
第二步,在resouce的templates下建立登錄頁面login.html
<!DOCTYPE HTML>
<!-- thymeleaf模板必須引入 -->
<!DOCTYPE HTML>
<!-- thymeleaf模板必須引入 -->
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>SpringBoot模版渲染</title>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/>
</head>
<body>
<form th:action="@{/login}" method="post" >
<input th:text="用戶名" type="text" name="username" />
<input th:text="密碼" type="password" name="password" />
<button type="submit" >提交</button>
<!-- ${session?.SPRING_SECURITY_LAST_EXCEPTION?.message} security自帶的錯誤提示信息 -->
<p th:if="${param.error}" th:text="${session?.SPRING_SECURITY_LAST_EXCEPTION?.message}" ></p>
</form>
</body>
</html>
第三步峻凫,添加security的配置文件渗鬼,為了debug登錄情況,對UserDetails荧琼、MyPasswordEncoder譬胎、UserDetailsService三個接口進(jìn)行了實(shí)現(xiàn)差牛,并在配置文件中進(jìn)行了配置
package com.example.demo.security;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.csrf.CsrfFilter;
import org.springframework.web.filter.CharacterEncodingFilter;
/**
* SpringSecurity的配置
* @author 程就人生
* @date 2019年5月26日
*/
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService myCustomUserService;
@Autowired
private MyPasswordEncoder myPasswordEncoder;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable();
http
//使用form表單post方式進(jìn)行登錄
.formLogin()
//登錄頁面為自定義的登錄頁面
.loginPage("/login")
//設(shè)置登錄成功跳轉(zhuǎn)頁面,error=true控制頁面錯誤信息的展示
.successForwardUrl("/index").failureUrl("/login?error=true")
.permitAll()
.and()
//允許不登陸就可以訪問的方法堰乔,多個用逗號分隔
.authorizeRequests().antMatchers("/test").permitAll()
//其他的需要授權(quán)后訪問
.anyRequest().authenticated();
//session管理,失效后跳轉(zhuǎn)
http.sessionManagement().invalidSessionUrl("/login");
//單用戶登錄偏化,如果有一個登錄了,同一個用戶在其他地方登錄將前一個剔除下線
//http.sessionManagement().maximumSessions(1).expiredSessionStrategy(expiredSessionStrategy());
//單用戶登錄镐侯,如果有一個登錄了侦讨,同一個用戶在其他地方不能登錄
http.sessionManagement().maximumSessions(1).maxSessionsPreventsLogin(true);
//退出時情況cookies
http.logout().deleteCookies("JESSIONID");
//解決中文亂碼問題
CharacterEncodingFilter filter = new CharacterEncodingFilter();
filter.setEncoding("UTF-8"); filter.setForceEncoding(true);
//
http.addFilterBefore(filter,CsrfFilter.class);
}
// @Bean
// public SessionInformationExpiredStrategy expiredSessionStrategy() {
// return new ExpiredSessionStrategy();
// }
@Bean
public DaoAuthenticationProvider daoAuthenticationProvider() {
DaoAuthenticationProvider bean = new DaoAuthenticationProvider();
//返回錯誤信息提示,而不是Bad Credential
bean.setHideUserNotFoundExceptions(true);
//覆蓋UserDetailsService類
bean.setUserDetailsService(myCustomUserService);
//覆蓋默認(rèn)的密碼驗(yàn)證類
bean.setPasswordEncoder(myPasswordEncoder);
return bean;
}
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(this.daoAuthenticationProvider());
}
}
在這個配置中苟翻,對登錄頁面進(jìn)行了設(shè)置韵卤,設(shè)置使用自定義的登錄頁面,在Controller需要添加對應(yīng)的頁面渲染袜瞬。
package com.example.demo.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;
/**
*
* @author 程就人生
* @date 2019年6月8日
*/
@RestController
public class IndexController {
@RequestMapping("/index")
public ModelAndView index(){
return new ModelAndView("/index");
}
@RequestMapping("/test")
public Object test(){
return "test";
}
/**
* 自定義登錄頁面
* @param error 錯誤信息顯示標(biāo)識
* @return
*
*/
@GetMapping("/login")
public ModelAndView login(String error){
ModelAndView modelAndView = new ModelAndView("/login");
modelAndView.addObject("error", error);
return modelAndView;
}
}
package com.example.demo.security;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
/**
* 登錄專用類,用戶登陸時怜俐,通過這里查詢數(shù)據(jù)庫
* 自定義類身堡,實(shí)現(xiàn)了UserDetailsService接口邓尤,用戶登錄時調(diào)用的第一類
* @author 程就人生
* @date 2019年5月26日
*/
@Component
public class MyCustomUserService implements UserDetailsService {
/**
* 登陸驗(yàn)證時,通過username獲取用戶的所有權(quán)限信息
* 并返回UserDetails放到spring的全局緩存SecurityContextHolder中贴谎,以供授權(quán)器使用
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//在這里可以自己調(diào)用數(shù)據(jù)庫汞扎,對username進(jìn)行查詢逢并,看看在數(shù)據(jù)庫中是否存在
MyUserDetails myUserDetail = new MyUserDetails();
if(StringUtils.isEmpty(username)){
throw new RuntimeException("用戶名不能為空灭必!");
}
if(!username.equals("admin")){
throw new RuntimeException("用戶名不存在虽界!");
}
myUserDetail.setUsername(username);
myUserDetail.setPassword("123456");
return myUserDetail;
}
}
package com.example.demo.security;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
/**
* 自定義的密碼加密方法瘦馍,實(shí)現(xiàn)了PasswordEncoder接口
* @author 程就人生
* @date 2019年5月26日
*/
@Component
public class MyPasswordEncoder implements PasswordEncoder {
@Override
public String encode(CharSequence charSequence) {
//加密方法可以根據(jù)自己的需要修改
return charSequence.toString();
}
@Override
public boolean matches(CharSequence charSequence, String s) {
return encode(charSequence).equals(s);
}
}
package com.example.demo.security;
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
/**
* 實(shí)現(xiàn)了UserDetails接口民假,只留必需的屬性烹植,也可添加自己需要的屬性
* @author 程就人生
* @date 2019年5月26日
*/
public class MyUserDetails implements UserDetails {
private static final long serialVersionUID = 1L;
//登錄用戶名
private String username;
//登錄密碼
private String password;
private Collection<? extends GrantedAuthority> authorities;
public void setUsername(String username) {
this.username = username;
}
public void setPassword(String password) {
this.password = password;
}
public void setAuthorities(Collection<? extends GrantedAuthority> authorities) {
this.authorities = authorities;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.authorities;
}
@Override
public String getPassword() {
return this.password;
}
@Override
public String getUsername() {
return this.username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
第四步橡羞,重新啟動項(xiàng)目担租,看一下登錄頁面的效果
一個很丑的登錄頁面溯香,這不是重點(diǎn)鲫构。重點(diǎn)是,登錄名和密碼正確時玫坛,頁面可以正確的跳轉(zhuǎn)结笨,輸入錯誤時,可以在登錄頁面進(jìn)行信息提示湿镀。
在MyCustomUserService類中炕吸,我們設(shè)置了用戶名為admin,密碼為123456勉痴;輸入其他的用戶名稱時赫模,提示用戶不存在;不輸入用戶名稱蒸矛,提示用戶不能為空瀑罗;密碼不是123456時扫外,提示密碼錯誤;輸入admin廓脆,123456時筛谚,頁面前往index頁面,下面進(jìn)行驗(yàn)證停忿。
第一步驾讲,測試不輸入用戶名,測試結(jié)果ok
第二步席赂,測試輸入錯誤的用戶名吮铭,測試結(jié)果ok
第三步,測試輸入正確的用戶名和錯誤的密碼颅停,測試結(jié)果ok
第四步谓晌,測試輸入正確的用戶名和密碼,測試結(jié)果ok
第五步癞揉,在index.html頁面中加個退出按鈕纸肉,測試一下退出操作,這里為了簡單喊熟,寫了一個表單
<!DOCTYPE HTML>
<!-- thymeleaf模板必須引入 -->
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>SpringBoot模版渲染</title>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/>
</head>
<body>
index
<form th:action="@{/logout}" method="get" >
<button type="submit" >退出</button>
</form>
</body>
</html>