Spring Boot2 + Spring Security5 自定义登录验证(3)

前言

上篇文章介绍了如何在Spring Boot引入Security

接下来,博主会简单的介绍下如何自定义登录配置

开始

首先,我们要有数据库的用户表,这里我用的是mysql5.6
表结构如下:

Spring Boot2 _ Spring Security5 自定义登录验证_3_ - 01.png

字段的话就不详细介绍了,相信看名字就能懂

整体demo结构如图:

Spring Boot2 _ Spring Security5 自定义登录验证_3_ - 02.png

虽然说是demo,但是本着严格务实的态度,也是遵守MVC的调用流程,所以包可能会有点繁琐

这里简单的说下这个登录验证的流程,以便大家更好的理解下面的代码,先看图:

Spring Boot2 _ Spring Security5 自定义登录验证_3_ - 03.png

绿色背景色为自定义实现的,也就是下面会出现的类方法
对于中间件那块来说是暂时没有的,可以不管先,后面的文章会引入,到时候再作介绍

当然,Spring Security认证的流程是没有那么简单的,这里只是给大家方便理解才简化了很多流程

下面开始展示代码

由于需要操作数据库,以及展示页面等,小编这里就需要引入持久层以及前端页面一些框架
这里博主用的是Spring Data Jpa,前端用的是Thymeleaf,Maven代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.7.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.javaymw</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.58</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

添加完相关的依赖,还是到项目的根目录下,执行maven的编译命令,把相关的jar下载下来:mvn clean compile

yml的配置不需要多大的修改,这次只是配置了数据源和jpa的一些基础属性,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
server:
tomcat:
uri-encoding: UTF-8
port: 8080

spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
username: root
password: root
jpa:
database: MYSQL
show-sql: true
hibernate:
ddl-auto: update
database-platform: org.hibernate.dialect.MySQL5InnoDBDialect

这里要注意的是,当引入的mysql-connector-java版本是6.0以上的话,那驱动就是:

com.mysql.cj.jdbc.Driver

中间是多个cj的
还有就是在数据源url后面要加上serverTimezone=UTC这条参数,否则也是会报错的

jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC

接下来就是编写spring security的配置类:

SecurityConfig .java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
/**
* Copyright 2020. javaymw.com Studio All Right Reserved
* <p>
* Create on 2020-06-05 21:58
* Created by zhaoxinguo
* Version 2.0.0
*/
package com.javaymw.demo.config;

import com.javaymw.demo.core.LoginValidateAuthenticationProvider;
import com.javaymw.demo.core.handler.LoginFailureHandler;
import com.javaymw.demo.core.handler.LoginSuccessHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

import javax.annotation.Resource;

/**
* @description: Spring Security 核心配置类
* @author zhaoxinguo
* @date 2020/6/5 21:57
*/
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

//自定义认证
@Resource
private LoginValidateAuthenticationProvider loginValidateAuthenticationProvider;

//登录成功handler
@Resource
private LoginSuccessHandler loginSuccessHandler;

//登录失败handler
@Resource
private LoginFailureHandler loginFailureHandler;

/**
* 权限核心配置
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
//基础设置
http.httpBasic()//配置HTTP基本身份验证
.and()
.authorizeRequests()
.anyRequest().authenticated()//所有请求都需要认证
.and()
.formLogin() //登录表单
.loginPage("/login")//登录页面url
.loginProcessingUrl("/login")//登录验证url
.defaultSuccessUrl("/index")//成功登录跳转
.successHandler(loginSuccessHandler)//成功登录处理器
.failureHandler(loginFailureHandler)//失败登录处理器
.permitAll();//登录成功后有权限访问所有页面
//关闭csrf跨域攻击防御
http.csrf().disable();
}

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//这里要设置自定义认证
auth.authenticationProvider(loginValidateAuthenticationProvider);
}

/**
* BCrypt加密
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

}

注意,Spring Security配置类必须继承WebSecurityConfigurerAdapter类才会生效

这里BCrypt加密方式是官方推荐使用的,还有就是Spring Security5.x是不需要配置加密方式的,因为它可以匹配多种加密方式以用来解密,只需要在密码前面加上加密方式即可,格式如下:

{加密方式}密文

例如:

  • {MD5}e10adc3949ba59abbe56e057f20f883e
  • {bcrypt}$2a101010bOZ5qFQS4OojeLUdb6K8.OU/KrVR8vzdo7QaCNKNG4oaIYUrAGKJ2

这样就可以实现兼容多个加密方式,可以说是挺人性化的,不过我这里还是规定死了哈哈哈哈

然后就是编写User实体类和UserService实现类:

User.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
/**
* Copyright 2020. javaymw.com Studio All Right Reserved
* <p>
* Create on 2020-06-05 22:01
* Created by zhaoxinguo
* Version 2.0.0
*/
package com.javaymw.demo.sys.entity;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import javax.persistence.*;
import java.util.Collection;

/**
* @description: TODO
* @author zhaoxinguo
* @date 2020/6/5 22:01
*/
@Entity
@Table(name = "sys_user")
public class User implements UserDetails {

//id
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column
protected Integer id;

//用户名
@Column
private String username;

//密码
@Column(nullable = false)
private String password;

/**
* 是否锁定
* true: 未锁定
* false: 锁定
*/
@Column
private boolean lockedFlag;

//security存储权限认证用的
@Transient
private Collection<? extends GrantedAuthority> authorities;

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}

@Override
public String getPassword() {
return password;
}

@Override
public String getUsername() {
return username;
}

/**
* 用户账号是否过期
* true: 未过期
* false: 已过期
*
* @return
*/
@Override
public boolean isAccountNonExpired() {
return true;
}

/**
* 用户账号是否被锁定
* true: 未锁定
* false: 锁定
*
* @return
*/
@Override
public boolean isAccountNonLocked() {
return lockedFlag;
}

/**
* 用户账号凭证(密码)是否过期
* 简单的说就是可能会因为修改了密码导致凭证过期这样的场景
* true: 过期
* false: 无效
*
* @return
*/
@Override
public boolean isCredentialsNonExpired() {
return true;
}

/**
* 用户账号是否被启用
* true: 启用
* false: 未启用
*
* @return
*/
@Override
public boolean isEnabled() {
return true;
}

public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public void setUsername(String username) {
this.username = username;
}

public void setPassword(String password) {
this.password = password;
}

public boolean isLockedFlag() {
return lockedFlag;
}

public void setLockedFlag(boolean lockedFlag) {
this.lockedFlag = lockedFlag;
}

public void setAuthorities(Collection<? extends GrantedAuthority> authorities) {
this.authorities = authorities;
}
}

这里要说明下,UserDetails是Spring Security提供的一个保存用户账号信息的接口,详情请看代码注释,因为有些地方是没有用到的,所以就写死了很多属性,大家可根据实际需求来修改使用

UserService.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/**
* Copyright 2020. javaymw.com Studio All Right Reserved
* <p>
* Create on 2020-06-05 22:04
* Created by zhaoxinguo
* Version 2.0.0
*/
package com.javaymw.demo.sys.service;

import com.javaymw.demo.sys.entity.User;
import com.javaymw.demo.sys.repository.UserRepository;
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.Service;
import javax.annotation.Resource;

/**
* @description: TODO
* @author zhaoxinguo
* @date 2020/6/5 22:04
*/
@Service
public class UserService implements UserDetailsService {

@Resource
private UserRepository userRepository;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findUserByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("不存在该用户!");
}
return user;
}
}

同理 UserDetailsService 也是是spring security提供的,这里实现了加载用户名称的方法,目的是为了获取用户信息,以便接下来的认证

UserRepository .java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* Copyright 2020. javaymw.com Studio All Right Reserved
* <p>
* Create on 2020-06-05 22:03
* Created by zhaoxinguo
* Version 2.0.0
*/
package com.javaymw.demo.sys.repository;

import com.javaymw.demo.sys.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

/**
* @description: TODO
* @author zhaoxinguo
* @date 2020/6/5 22:03
*/
@Repository
public interface UserRepository extends JpaRepository<User, Integer> {

User findUserByUsername(String username);
}

这个相信不用多说了吧

下面就是自定义认证的核心代码:

LoginValidateAuthenticationProvider.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
/**
* Copyright 2020. javaymw.com Studio All Right Reserved
* <p>
* Create on 2020-06-05 21:59
* Created by zhaoxinguo
* Version 2.0.0
*/
package com.javaymw.demo.core;

import com.javaymw.demo.sys.entity.User;
import com.javaymw.demo.sys.service.UserService;
import org.springframework.security.authentication.*;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;

/**
* @description: 自定义认证核心类
* @author zhaoxinguo
* @date 2020/6/5 21:59
*/
@Component
public class LoginValidateAuthenticationProvider implements AuthenticationProvider {

@Resource
private UserService userService;

/**
* 解密用的
*/
@Resource
private PasswordEncoder passwordEncoder;

/**
* 进行身份验证
* @param authentication
* @return
* @throws AuthenticationException
*/
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
//获取输入的用户名
String username = authentication.getName();
//获取输入的明文
String rawPassword = (String) authentication.getCredentials();
//查询用户是否存在
User user = (User) userService.loadUserByUsername(username);
if (!user.isEnabled()) {
throw new DisabledException("该账户已被禁用,请联系管理员");
} else if (!user.isAccountNonLocked()) {
throw new LockedException("该账号已被锁定");
} else if (!user.isAccountNonExpired()) {
throw new AccountExpiredException("该账号已过期,请联系管理员");
} else if (!user.isCredentialsNonExpired()) {
throw new CredentialsExpiredException("该账户的登录凭证已过期,请重新登录");
}
//验证密码
if (!passwordEncoder.matches(rawPassword, user.getPassword())) {
throw new BadCredentialsException("输入密码错误!");
}
return new UsernamePasswordAuthenticationToken(user, rawPassword, user.getAuthorities());
}

@Override
public boolean supports(Class<?> authentication) {
//确保authentication能转成该类
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
}

这里通过实现AuthenticationProvider 认证授权类,以达到自定义登录的效果,注意,这里是结合了之前实现的loadUserByUsername方法去获取用户信息,以及用户状态去判断登录是否能通过

接下来就是handler代码:

LoginSuccessHandler.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/**
* Copyright 2020. javaymw.com Studio All Right Reserved
* <p>
* Create on 2020-06-05 22:01
* Created by zhaoxinguo
* Version 2.0.0
*/
package com.javaymw.demo.core.handler;

import com.alibaba.fastjson.JSONObject;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;

/**
* @description: 登陆成功处理handler
* @author zhaoxinguo
* @date 2020/6/5 22:00
*/
@Component
public class LoginSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {

@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
//登录成功返回
Map<String, Object> paramMap = new HashMap<>(2);
paramMap.put("code", "200");
paramMap.put("message", "登录成功!");
//设置返回请求头
response.setContentType("application/json;charset=utf-8");
//写出流
PrintWriter out = response.getWriter();
out.write(JSONObject.toJSONString(paramMap));
out.flush();
out.close();
}
}

LoginFailureHandler.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/**
* Copyright 2020. javaymw.com Studio All Right Reserved
* <p>
* Create on 2020-06-05 22:00
* Created by zhaoxinguo
* Version 2.0.0
*/
package com.javaymw.demo.core.handler;

import com.alibaba.fastjson.JSONObject;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;

/**
* @description: 登录失败处理handler
* @author zhaoxinguo
* @date 2020/6/5 22:00
*/
@Component
public class LoginFailureHandler extends SimpleUrlAuthenticationFailureHandler {

@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
//登录失败信息返回
Map<String, Object> paramMap = new HashMap<>(2);
paramMap.put("code", "500");
paramMap.put("message", exception.getMessage());
//设置返回请求头
response.setContentType("application/json;charset=utf-8");
//写出流
PrintWriter out = response.getWriter();
out.write(JSONObject.toJSONString(paramMap));
out.flush();
out.close();
}
}

那么到这里,也已经差不多了,现在还差的是登录的前端页面和一些效果

login.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录页</title>
</head>
<body>
<h2>登录页</h2>
<form id="loginForm" action="/login" method="post">
用户名:<input type="text" id="username" name="username"><br/><br/>
密&nbsp;&nbsp;&nbsp;码:<input type="password" id="password" name="password"><br/><br/>
<button id="loginBtn" type="button">登录</button>
</form>
<script src="http://libs.baidu.com/jquery/2.1.4/jquery.min.js"></script>
<script type="text/javascript">
$("#loginBtn").click(function () {
$.ajax({
type: "POST",
url: "/login",
data: $("#loginForm").serialize(),
dataType: "JSON",
success: function (data) {
console.log(data);
//window.location.href = "/index";
}
});
});
</script>
</body>
</html>

这里为了方便演示,就直接在前端输出登录信息,下面看看演示图:

Spring Boot2 _ Spring Security5 自定义登录验证_3_ - 04.png

这里我在数据库加了条登录数据,数据默认用户脚本如下,用户名、密码(admin/123456):

1
INSERT INTO `test`.`sys_user` (`id`, `locked_flag`, `password`, `username`) VALUES ('1', b'1', '$2a$10$UNVzCpRC3ND2XrCu8rZWJ.OkAzpyP651itODJiKTMOpqLMWrTLcEi', 'admin');

这里我在数据库加了条登录数据,当填正确账号点击登录的时候,显示是成功的

Spring Boot2 _ Spring Security5 自定义登录验证_3_ - 05.png

随便输入个错误的密码则是:

Spring Boot2 _ Spring Security5 自定义登录验证_3_ - 06.png

输入个不存在的用户名则是:

Spring Boot2 _ Spring Security5 自定义登录验证_3_ - 07.png

修改用户状态为锁定则是:

Spring Boot2 _ Spring Security5 自定义登录验证_3_ - 08.png

那么基本代码和效果也演示完毕了

源码获取方式加加入QQ交流群(715224124),进群找群主要源码,如果有问题,可以提出疑问,群主会尽量帮助解决~

希望能帮助到大家,如果有不好或者错误的地方希望能多多提出,谢谢大家~

MiCai wechat
扫一扫,关注微信订阅号
坚持原创技术分享,您的支持将鼓励我继续创作!