大致分為三部分:資料庫認證,自定義登入頁,自定義過濾器
SpringSecurity主要實現UserDetailsService來驗證登入的使用者資訊,和Security的設定類來對登入方式和資源進行限制。
案例包含利用資料庫進行登入驗證、URL存取限制、自定義登入頁和利用ajax方式登入、實現自定義過濾器對驗證碼進行驗證,完整程式碼在https://github.com/say-hey/springboot-security-concise
需要自定義的User類繼承UserDetails,然後實現方法,但在某些案例中也沒有繼承
同時在資料庫中新增相應欄位,如是否過期是否鎖定等
/**
* 使用者表
* 使用者表和角色表的對應關係,
*/
@Data
@Entity
@AllArgsConstructor
@NoArgsConstructor
@Table(name = "user")
// 自定義的User可以實現 implements UserDetails 介面,需要完成方法如是否可用,是否鎖定,是否過期,角色集合等,同時在資料庫中新增這些欄位
// 實現這個方法可用於擴充套件,也可以不實現
public class User implements UserDetails{
@Id
// 主鍵自動增長
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
Integer id;
@Column(name = "username")
String username;
@Column(name = "password")
String password;
// 過期
@Column(name = "isAccountNonExpired")
Boolean isAccountNonExpired;
// 鎖定
@Column(name = "isAccountNonLocked")
Boolean isAccountNonLocked;
// 憑證
@Column(name = "isCredentialsNonExpired")
Boolean isCredentialsNonExpired;
// 啟用
@Column(name = "isEnabled")
Boolean isEnabled;
// 許可權
// List<GrantedAuthority> authorities;
/**
* 多對多關係會在建立使用者和新角色時級聯新增,關聯表為user_role,當前物件在關聯表對應的外來鍵,和另一方在關聯表中對應的外來鍵
* cascade:級聯操作,如儲存、刪除時級聯的行為
* joinColumns:在關聯表中的外來鍵名
* inverseJoinColumns:另一方在關聯表中的外來鍵名
*/
@ManyToMany(targetEntity = Role.class, cascade = CascadeType.MERGE, fetch = FetchType.LAZY)
@JoinTable(name = "user_role",
joinColumns = {@JoinColumn(name = "u_id", referencedColumnName = "id")},
inverseJoinColumns = {@JoinColumn(name = "r_id", referencedColumnName = "id")})
List<Role> roles = new ArrayList<>();
/**
* 重寫toString()方法,否則在sout輸出時,會導致兩個物件的toString()相互呼叫,現在需要去掉一方的關聯欄位輸出
* java.lang.StackOverflowError
* @return
*/
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", password='" + password + '\'' +
", isAccountNonExpired=" + isAccountNonExpired +
", isAccountNonLocked=" + isAccountNonLocked +
", isCredentialsNonExpired=" + isCredentialsNonExpired +
", isEnabled=" + isEnabled +
", roles=" + roles +
'}';
}
// 實現UserDetails後的方法
/**
* 獲取許可權,這裡使用的是GrantedAuthority類,在UserDetailsService中出現,用於組裝角色許可權資訊
*
* roles: [Role{id=1, role='Cat'}, Role{id=2, role='Dog'}]
* authorities: [ROLE_Dog, ROLE_Cat]
*
* @return
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<Role> roles = this.getRoles();
Set<GrantedAuthority> authorities = new HashSet<>();
for (Role role : roles) {
authorities.add(new SimpleGrantedAuthority("ROLE_"+role.getRole()));
}
return authorities;
}
/**
* 賬戶是否過期
* @return
*/
@Override
public boolean isAccountNonExpired() {
return false;
}
/**
* 賬戶是否鎖定
* @return
*/
@Override
public boolean isAccountNonLocked() {
return false;
}
/**
* 憑證是否過期
* @return
*/
@Override
public boolean isCredentialsNonExpired() {
return false;
}
/**
* 是否啟用
* @return
*/
@Override
public boolean isEnabled() {
return false;
}
}
具體實現檢視下一小節
SpringSecurity中有一個UserDetail介面,高度抽象使用者資訊類,它返回一個User類,和自定義user內容相似,包括username,password,authorities(角色、許可權,繼承GrantedAuthority)集合
其中,角色和許可權內容表達不同,角色:admin許可權:ROLE_ADMIN
實現介面UserDetailService介面,完成loadUserByUsername方法,返回User
SpringSecurity在登入時會自動呼叫方法,去資料庫中查詢出資料並驗證
@Service
public class SecurityUserDetailsServiceImpl implements UserDetailsService {
@Transactional
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findUserByUsername(username);
if(user == null){
throw new UsernameNotFoundException("使用者 " + username + " 登入失敗,使用者名稱不存在!");
}
// System.out.println("登入使用者:" + ((Role)user.getRoles()).getRole());
// 方式一:新增許可權
List<Role> roles = user.getRoles();
Set<GrantedAuthority> authorities = new HashSet<>();
for (Role role : roles) {
authorities.add(new SimpleGrantedAuthority("ROLE_"+role.getRole()));
}
// 方法二:在自定義的User實現UserDetails後,利用上方方式實現getAuthorities()方法,直接返回
Collection<? extends GrantedAuthority> authorities1 = user.getAuthorities();
// 許可權和角色在字首上不同,許可權會自動加上字首ROLE_,roles()方法點進去就是GrantedAuthority
// GrantedAuthority : ROLE_admin
// Role : admin
return org.springframework.security.core.userdetails.User
.withUsername(user.getUsername())
.password(user.getPassword())
.authorities(authorities)
.build();
}
}
對於role和authorities:
roles: [Role{id=1, role='Cat'}, Role{id=2, role='Dog'}]
authorities: [ROLE_Dog, ROLE_Cat]
實現資料庫認證之後,設定URL許可權,就可以在網頁進行許可權控制
方式一:舊方式,使用預設登入頁,在實現SecurityConfigurerAdapter類的cofnigure(HttpSecurity)方法中設定
// 實現SecurityConfigurerAdapter類
public void configure(HttpSecurity http){
http.authorizeHttpRequests()
.requestMatchers("/home").hasRole("USER")
.requestMatchers("/home/l1/**").hasRole("Dog")
.requestMatchers("/home/l2/**").hasRole("Cat")
.and()
.formLogin();
}
// 「formLogin()」已棄用並標記為刪除
方式二:在SecurityConfig設定類中注入設定HttpSecurity
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(auth->{
// 設定url許可權,注意所有許可權的設定順序
auth.requestMatchers("/home").permitAll();
auth.requestMatchers("/home/l0").hasRole("USER");
auth.requestMatchers("/home/l1/**").hasRole("Dog");
auth.requestMatchers("/home/l2/**").hasRole("Cat");
auth.anyRequest().authenticated();
})
.build();
}
請求連結
<h2>Welcome Home</h2>
<!-- gn cheems -->
<a href="/home/l0">a dog/cat</a><br>
<a href="/home/l1">a dog</a><br>
<a href="/home/l2">a cat</a><br>
Controller
@RestController
public class HomeController {
@GetMapping("/home/l0")
public String l0(){
return "you is a dog/cat";
}
@GetMapping("/home/l1")
public String l1(){
return "you is a dog";
}
@GetMapping("/home/l2")
public String l2(){
return "you is a cat";
}
}
檢視過濾器類UsernamePasswordAuthenticationFilter,裡面設定了預設的登入頁的資訊,只要規則匹配就會自動驗證登入資訊
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/login", "POST");
private String usernameParameter = "username";
private String passwordParameter = "password";
private boolean postOnly = true;
// ...
}
自定義登入頁的標籤也要用username,password屬性
<!-- 這裡表單傳送的請求是post,在SecurityConfig.loginProcessingUrl和indexController.login自定義的登入頁是get/login,表單請求可以更改名字,避免混淆-->
<form th:action="@{/login}" method="post">
<div>
<input type="text" name="username" placeholder="Username"/>
</div>
<div>
<input type="password" name="password" placeholder="Password"/>
</div>
<input type="submit" value="Log in" />
</form>
設定security,注入HttpSecurity引數
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(auth->{
// 設定url許可權,注意所有許可權的設定順序
auth.requestMatchers("/home").permitAll();
auth.requestMatchers("/home/l0").hasRole("USER");
auth.requestMatchers("/home/l1/**").hasRole("Dog");
auth.requestMatchers("/home/l2/**").hasRole("Cat");
auth.anyRequest().authenticated();
})
.formLogin(conf->{
// 自定義表單登入頁,這個是網頁
// https://docs.spring.io/spring-security/reference/servlet/authentication/passwords/form.html
conf.loginPage("/login");
// 表單登入請求,這個是url請求
conf.loginProcessingUrl("/login");
// 預設登入成功頁
conf.defaultSuccessUrl("/home");
// 登入相關請求不需要認證
conf.permitAll();
})
.logout(conf->{
// 登出請求
conf.logoutUrl("/logout");
conf.logoutSuccessUrl("/login");
conf.permitAll();
})
// 使用自定義的userDetails認證過程,
// .userDetailsService(null)
.csrf(AbstractHttpConfigurer::disable)// 關閉跨站請求偽造保護功能
.build();
}
<script type="text/javascript" src="/js/jquery-3.7.0.min.js"></script>
<script type="text/javascript">
$(function (){
$("#btnLogin").click(function () {
console.log("ajax")
var uname = $("#username").val();
var pwd = $("#password").val();
$.ajax({
url:"/login",
type:"POST",
data:{
"username":uname,
"password":pwd
},
dataType:"json",
success:function (res) {
alert(res.status +":"+res.msg)
}
})
})
})
</script>
<div>
使用Ajax登入,json傳遞資料<br>
使用者名稱:<input type="text" id="username"><br>
密 碼:<input type="password" id="password"><br>
<button id="btnLogin">登入</button><br>
</div>
在security設定類中通過靜態資源認證
// 靜態資源
auth.requestMatchers("/js/**").permitAll();
認證處理器,自定義請求認證成功或失敗後的動作
/**
* security登入認證成功處理器
*/
@Component
public class SecurityAuthSuccessHandler implements AuthenticationSuccessHandler {
/**
* 驗證成功後執行
* @param request 請求物件
* @param response 響應物件
* @param authentication security驗證成功後的封裝物件,包括使用者的資訊
* @throws IOException
* @throws ServletException
*/
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
// 登入的使用者驗證成功後執行
response.setContentType("text/json;charset=utf-8");
PrintWriter writer = response.getWriter();
writer.println("{\"msg\":\"登入成功!\"}");
writer.flush();
writer.close();
}
}
/**
* security登入認證失敗處理器
*/
@Component
public class SecurityAuthFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
// 登入的使用者驗證失敗後執行
response.setContentType("text/json;charset=utf-8");
PrintWriter writer = response.getWriter();
writer.println("{\"msg\":\"登入失敗(使用者名稱或密碼錯誤)!\"}");
writer.flush();
writer.close();
}
}
在security設定類中通過靜態資源認證
// 靜態資源
auth.requestMatchers("/js/**").permitAll();
注意,使用了handler處理器,就不要設定預設登入頁,否則不起作用
// 使用handler類
conf.successHandler(successHandler);
conf.failureHandler(failureHandler);
// 預設登入成功頁,使用了handler,就不要使用預設登入頁,否則handler不起作用
// conf.defaultSuccessUrl("/home");
在用ajax的過程中使用json傳遞資料
建立vo類物件,傳遞資料
@Data
public class Result {
// 0成功 1失敗
Integer code;
// 200 成功 500失敗
Integer status;
// 訊息
String msg;
}
處理器
@Component
public class SecurityAuthSuccessHandler implements AuthenticationSuccessHandler {
/**
* 驗證成功後執行
* @param request 請求物件
* @param response 響應物件
* @param authentication security驗證成功後的封裝物件,包括使用者的資訊
* @throws IOException
* @throws ServletException
*/
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
// 登入的使用者驗證成功後執行
response.setContentType("text/json;charset=utf-8");
Result result = new Result();
result.setCode(0);
result.setStatus(200);
result.setMsg("登入成功");
// 使用jsckson
ObjectMapper mapper = new ObjectMapper();
ServletOutputStream outputStream = response.getOutputStream();
mapper.writeValue(outputStream, result);
outputStream.flush();
outputStream.close();
// PrintWriter writer = response.getWriter();
// writer.println("{\"msg\":\"登入成功!\"}");
// writer.flush();
// writer.close();
}
}
在使用者名稱和密碼下方新增驗證碼輸入,在controller中生成驗證碼圖片,然後響應給網頁
/**
* 生成驗證碼響應到頁面
*/
@Controller
@RequestMapping("/captcha")
public class ChptchaController {
// 生成驗證碼的屬性
// 寬度
private int width = 120;
// 高度
private int height = 30;
// 內容在圖片中的起始位置
private int drawY = 20;
// 文字的間隔
private int space = 15;
// 驗證碼文字個數
private int charCount = 6;
// 驗證碼內容陣列 注意數位0和字母O容易混淆,最好註釋掉
private String chars[] = {"A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P"
,"Q","R","S","T","U","V","W","X","Y","Z","0","1","2","3","4","5","6","7","8","9"};
/**
* 繪製一個圖片,將圖片響應給請求
* @param request
* @param response
*/
@GetMapping("/code")
public void makeCaptchaCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
// 建立一個背景透明的圖片,圖片格式使用rgb表示顏色,畫布
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
// 獲取畫筆
Graphics graphics = image.getGraphics();
// 設定畫筆顏色 白色
graphics.setColor(Color.white);
// 把畫布塗成白色 fillRect(矩形的起始x,矩形的起始y,矩形的寬度,矩形的高度)
graphics.fillRect(0, 0, width, height);
// 畫內容
// 建立字型
Font font = new Font("宋體", Font.BOLD, 18);
// 畫筆設定字型和顏色
graphics.setFont(font);
graphics.setColor(Color.black);
// 獲取隨機值
int ran = 0;
int len = chars.length;
StringBuffer stringBuffer = new StringBuffer();
for(int i = 0; i < charCount; i++){
ran = new Random().nextInt(len);
// 儲存隨機值
stringBuffer.append(chars[ran]);
// 設定隨機顏色
graphics.setColor(randomColor());
// 畫的內容,間隔,起始
graphics.drawString(chars[ran], (i+1)*space, drawY);
}
// 繪製干擾線
for(int i = 0; i < 4; i++){
graphics.setColor(randomColor());
int line[] = randomLine();
graphics.drawLine(line[0], line[1], line[2], line[3]);
}
// 生成的驗證碼存到session
request.getSession().setAttribute("code", stringBuffer.toString());
System.out.println("captcha: " + stringBuffer.toString());
// 設定響應沒有快取
response.setHeader("Pragma", "no-cache");
response.setHeader("Cache-Control", "no-cache");
response.setDateHeader("Expires", 0);
// 設定響應格式
response.setContentType("image/png");
// 輸出影象 w(輸出的影象,影象格式,輸出到哪)
ServletOutputStream outputStream = response.getOutputStream();
ImageIO.write(image, "png", outputStream);
outputStream.flush();
outputStream.close();
}
/**
* 生成隨機顏色
* @return
*/
public Color randomColor(){
Random random = new Random();
int r = random.nextInt(255);
int g = random.nextInt(255);
int b = random.nextInt(255);
return new Color(r, g, b);
}
/**
* 生成干擾線的隨機起始點
* @return
*/
public int[] randomLine(){
Random random = new Random();
int x1 = random.nextInt(width/2);
int y1 = random.nextInt(height);
int x2 = random.nextInt(width);
int y2 = random.nextInt(height);
return new int[]{x1, y1, x2, y2};
}
}
通過驗證
// 驗證碼
auth.requestMatchers("/captcha/**").permitAll();
在前端頁面新增驗證碼
<script type="text/javascript" src="/js/jquery-3.7.0.min.js"></script>
<script type="text/javascript">
$(function (){
$("#btnLogin").click(function () {
console.log("ajax")
var uname = $("#username").val();
var pwd = $("#password").val();
// 使用者輸入驗證碼
var textcode = $("#textcode").val();
$.ajax({
url:"/login",
type:"POST",
data:{
"username":uname,
"password":pwd,
"code":textcode
},
dataType:"json",
success:function (res) {
alert(res.status +":"+res.msg)
}
})
})
})
function changeCode(){
var url = "/captcha/code?t=" + new Date();
$("#imageCode").attr("src", url);
}
</script>
// ...
<div>
使用Ajax登入,json傳遞資料<br>
使用者名稱:<input type="text" id="username"><br>
密 碼:<input type="password" id="password"><br>
驗證碼:<input type="text" id="textcode">
<img src="/captcha/code" id="imageCode"/>
<a href="javascript:void(0)" onclick="changeCode()">重新獲取</a><br>
<button id="btnLogin">登入</button><br>
</div>
驗證碼例外處理,在過濾器處理驗證碼之前
/**
* 驗證碼例外處理,在過濾器處理驗證碼之前
*/
public class VerificationException extends AuthenticationException {
public VerificationException(){
super("驗證碼錯誤,請重新輸入!");
}
}
Security中有很多過濾器,例如表單登入驗證使用的UsernamePasswordAuthenticationFilter,而驗證碼在表單登入驗證之前使用,所以需要自定義一個過濾器,然後放入整個過濾器鏈中,並且在UsernamePasswordAuthenticationFilter之前
使用OncePerRequestFilter,一次性過濾器,出現異常呼叫handler處理器
/**
* 驗證碼過濾器,使用在UsernamePasswordAuthenticationFilter之前
*/
public class VerificationFilter extends OncePerRequestFilter {
// 登入失敗的handler,在過濾器丟擲異常時使用
private SecurityAuthFailureHandler failureHandler = new SecurityAuthFailureHandler();
/**
* 驗證碼過濾器
* @param request
* @param response
* @param filterChain
* @throws ServletException
* @throws IOException
*/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 缺少登入成功,錯誤提示有問題!!原因是設定了defaultSuccessUrl(),同樣的有failurehandler也不要設定預設的錯誤頁
// 驗證碼只在登入的過程中才使用這個過濾器
String requestURI = request.getRequestURI();
// 如果登入頁和表單登入請求都使用/login,那麼此處要判斷是去登入頁(GET)還是表單登入請求(POST)
String method = request.getMethod();
if(!"/login".equals(requestURI) || "GET".equals(method)){
// 不是登入操作,不經過這個過濾器
filterChain.doFilter(request, response);
}else{
try{
// 驗證驗證碼
verificationCode(request);
// 通過
filterChain.doFilter(request, response);
}catch (VerificationException e){
// 驗證出現異常時,跳轉到表單登入失敗的處理器SecurityAuthFailureHandler中
// 1.在filter中新增handler屬性,在這裡呼叫
// 2.在SecurityAuthFailureHandler中修改,新增一個vo.Result屬性,然後判斷是正常的handler還是第三方異常跳轉過去的
Result result = new Result();
result.setCode(1);
result.setStatus(501);
result.setMsg("驗證碼錯誤,請重新輸入!!");
failureHandler.setResult(result);
failureHandler.onAuthenticationFailure(request, response, e);
}
}
}
private void verificationCode(HttpServletRequest request) throws VerificationException {
// 獲取請求中的驗證碼Code
String requestCode = request.getParameter("code");
// 獲取session中的驗證碼Code
String sessionCode = "";
HttpSession session = request.getSession();
Object code = session.getAttribute("code");
if(code != null){
sessionCode = (String) code;
}
System.out.println("Verificate Captcha: session:" + sessionCode + " |request:" + requestCode);
// 一次性驗證碼,使用後銷燬
if(!StringUtils.isEmpty(sessionCode)){
// 能獲取到session中的驗證碼,說明已經在頁面生成了,現在就不能再用了
session.removeAttribute("code");
}
// 判斷驗證碼code是否正確
if(StringUtils.isEmpty(requestCode) || StringUtils.isEmpty(sessionCode) || !requestCode.equals(sessionCode)){
// 驗證失敗
throw new VerificationException();
}
}
}
修改handler處理器,判斷一下是否第三方呼叫(如驗證碼異常)
/**
* security登入認證失敗處理器
*/
@Component
public class SecurityAuthFailureHandler implements AuthenticationFailureHandler {
// 新增result屬性,可以讓第三方異常呼叫,展示異常資訊
private Result result;
public Result getResult() {
return result;
}
public void setResult(Result result) {
this.result = result;
}
/**
* 驗證失敗後執行
* @param request 請求物件
* @param response 響應物件
* @param exception security驗證失敗後的封裝物件,包括使用者的資訊
* @throws IOException
* @throws ServletException
*/
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
// 登入的使用者驗證失敗後執行
response.setContentType("text/json;charset=utf-8");
System.out.println("failure handler...");
// 判斷是否自定義的result,還是第三方異常呼叫的result,第三方異常呼叫時,result已經有值了
if(result == null){
Result localResult = new Result();
localResult.setCode(1);
localResult.setStatus(500);
localResult.setMsg("登入失敗(使用者名稱或密碼錯誤)!");
result = localResult;
}
// 使用jsckson
ObjectMapper mapper = new ObjectMapper();
ServletOutputStream outputStream = response.getOutputStream();
mapper.writeValue(outputStream, result);
outputStream.flush();
outputStream.close();
}
}
Security設定過濾器,注意用了handler處理器,就不要設定預設登入頁
@EnableWebSecurity
@Configuration
public class SecurityConfig {
// 驗證成功和失敗處理器
@Autowired
SecurityAuthSuccessHandler successHandler;
@Autowired
SecurityAuthFailureHandler failureHandler;
/**
* 密碼編碼器
* @return
*/
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder(){
return new BCryptPasswordEncoder();
}
/**
*
* 之前的做法有在configure(AuthenticationManagerBuilder)中設定auth.userDetailsService(myDetailsService).passwordEncoder(bcry)
* 在configure(HttpSecurity)中設定http.authorizeHttpRequests()認證
* 現在同樣使用HttpSecurity引數,HttpSecurity:具體的許可權控制規則設定
* @param http
* @return
* @throws Exception
*/
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(auth->{
// 設定url許可權,注意所有許可權的設定順序
auth.requestMatchers("/home").permitAll();
// 驗證碼
auth.requestMatchers("/captcha/**").permitAll();
// 靜態資源
auth.requestMatchers("/js/**").permitAll();
auth.requestMatchers("/home/l0").hasRole("USER");
auth.requestMatchers("/home/l1/**").hasRole("Dog");
auth.requestMatchers("/home/l2/**").hasRole("Cat");
auth.anyRequest().authenticated();
})
.formLogin(conf->{
// 自定義表單登入頁
// https://docs.spring.io/spring-security/reference/servlet/authentication/passwords/form.html
conf.loginPage("/login");
// 表單登入請求
conf.loginProcessingUrl("/login");
// 登入成功處理器,取消defaultSuccessUrl預設登入成功頁可以看到效果,如登入失敗處理器類似
// conf.successHandler(authenticationSuccessHandler());
// 登入失敗處理器,但此處不能在表單上方顯示error資訊
// conf.failureHandler(authenticationFailureHandler());
// 使用handler類
conf.successHandler(successHandler);
conf.failureHandler(failureHandler);
// 預設登入成功頁,使用了handler,就不要使用預設登入頁,否則handler不起作用
// conf.defaultSuccessUrl("/home");
// 登入相關請求不需要認證
conf.permitAll();
})
.logout(conf->{
// 登出請求
conf.logoutUrl("/logout");
conf.logoutSuccessUrl("/login");
conf.permitAll();
})
// 使用自定義過濾器,並且
.addFilterBefore(new VerificationFilter(), UsernamePasswordAuthenticationFilter.class)
// 使用自定義的userDetails認證過程,
// .userDetailsService(null)
.csrf(AbstractHttpConfigurer::disable)// 關閉跨站請求偽造保護功能
.build();
}
}
html新增驗證碼
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org">
<head>
<title>Welcome</title>
<script type="text/javascript" src="/js/jquery-3.7.0.min.js"></script>
<script type="text/javascript">
$(function (){
$("#btnLogin").click(function () {
console.log("ajax")
alert("ajax")
var uname = $("#username").val();
var pwd = $("#password").val();
// 使用者輸入驗證碼
var textcode = $("#textcode").val();
$.ajax({
url:"/login",
type:"POST",
// async: false,
data:{
"username":uname,
"password":pwd,
"code":textcode
},
dataType:"json",
success:function(res) {
console.log(res)
alert(res.status +":"+res.msg)
}
})
})
})
function changeCode(){
// 防止快取
var url = "/captcha/code?t=" + new Date();
$("#imageCode").attr("src", url);
}
</script>
</head>
<body>
<h1>Welcome Log In</h1>
<div th:if="${param.error}">
Invalid username and password.</div>
<div th:if="${param.logout}">
You have been logged out.</div>
<!-- 這裡表單傳送的請求是post,在SecurityConfig.loginProcessingUrl和indexController.login自定義的登入頁是get/login,表單請求可以更改名字,避免混淆-->
<form th:action="@{/login}" method="post">
<div>
<input type="text" name="username" placeholder="Username"/>
</div>
<div>
<input type="password" name="password" placeholder="Password"/>
</div>
<input type="submit" value="Log in" />
</form>
<br>
<div>
使用Ajax登入,json傳遞資料<br>
使用者名稱:<input type="text" id="username"><br>
密 碼:<input type="password" id="password"><br>
驗證碼:<input type="text" id="textcode">
<img src="/captcha/code" id="imageCode"/>
<a href="javascript:void(0)" onclick="changeCode()">重新獲取</a><br>
<button id="btnLogin">登入</button><br>
</div>
</body>
</html>
完整程式碼在https://github.com/say-hey/springboot-security-concise