Spring Security 示例

本文将介绍几个 Spring Security 应用示例。

对应代码:

codewld/Spring-Security-Demo: Spring Security 在 Spring Boot 中的集成示例

一、简单示例

Simple-Example

1. 导入依赖

在 pom.xml 中,添加依赖信息如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<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-security</artifactId>
</dependency>

2. HTML 模板文件

本示例使用 thymeleaf 作为 HTML 模板引擎

(1) index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Index</title>
</head>
<body>
<h1>主页,任何人都可以访问</h1>
<a href="/index">主页</a>
<a href="/user">用户页面</a>
<a href="/admin">管理员页面</a>
<a href="/login">登录页面</a>
</body>
</html>

(2) 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
31
32
33
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>index</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<h1>登陆页面</h1>
<div style="text-align: center;margin:0 auto;width: 1000px; ">
<form th:action="@{/login}" method="post">
<div>
用户名: <input type="text" name="username"/>
</div>
<div>
密码: <input type="password" name="password"/>
</div>
<div th:if="${param.error}">
<p style="text-align: center; color: red" class="text-danger">登录失败,账号或密码错误!</p>
</div>
<div th:if="${result}">
<p style="text-align: center" class="text-success" th:text="${result}"></p>
</div>
<div>
<input type="submit" value="登陆"/>
</div>
</form>
<a href="/index">主页</a>
<a href="/user">用户页面</a>
<a href="/admin">管理员页面</a>
<a href="/login">登录页面</a>
</div>
</body>
</html>

(3) user/user.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>User</title>
</head>
<body>
<h1>用户页面,需要USER权限</h1>
<p>已经成功登陆,你的用户名是:</p>
<p th:text="${username}"></p>
<form th:action="@{/logout}" method="post">
<button class="btn btn-danger" style="margin-top: 20px">退出登录</button>
</form>
<a href="/index">主页</a>
<a href="/user">用户页面</a>
<a href="/admin">管理员页面</a>
<a href="/login">登录页面</a>
</body>
</html>

(4) admin/admin.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Admin</title>
</head>
<body>
<h1>管理员页面,需要ADMIN权限</h1>
<p>已经成功登陆,你的用户名是:</p>
<p th:text="${username}">wxb</p>
<form th:action="@{/logout}" method="post">
<button class="btn btn-danger" style="margin-top: 20px">退出登录</button>
</form>
<a href="/index">主页</a>
<a href="/user">用户页面</a>
<a href="/admin">管理员页面</a>
<a href="/login">登录页面</a>
</body>
</html>

(5) error/403.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>403</title>
</head>
<body>
<h1 style="color: red">403错误,权限不足,请使用合适用户登录</h1>
<a href="/index">主页</a>
<a href="/user">用户页面</a>
<a href="/admin">管理员页面</a>
<a href="/login">登录页面</a>
</body>
</html>

3. Controller

(1) HomeController

1
2
3
4
5
6
7
8
9
10
11
12
@Controller
public class HomeController {
@RequestMapping({"/", "/index"})
public String index(){
return "index";
}

@RequestMapping("/login")
public String login(){
return "login";
}
}

(2) UserController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Controller
public class UserController {

@RequestMapping("/user")
public String user(@AuthenticationPrincipal UserDetails user, Model model){
model.addAttribute("username", user.getUsername());
return "user/user";
}

@RequestMapping("/admin")
public String admin(@AuthenticationPrincipal UserDetails user, Model model){
model.addAttribute("username", user.getUsername());
return "admin/admin";
}
}

4. 配置类

新建 Spring Security 配置类,如下:

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
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 在内存中创建一个名为 "user" 的用户,密码为 "pwd",拥有 "USER" 权限
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
.withUser("user").password(new BCryptPasswordEncoder().encode("pwd")).roles("USER");
// 在内存中创建一个名为 "admin" 的用户,密码为 "pwd",拥有 "USER" 和"ADMIN"权限
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
.withUser("admin").password(new BCryptPasswordEncoder().encode("pwd")).roles("USER", "ADMIN");
}

@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
// "/", "/index", "/error" 不需要权限即可访问
.antMatchers("/", "/index", "/error").permitAll()
// "/user" 及其以下所有路径,都需要 "USER" 权限
.antMatchers("/user/**").hasRole("USER")
// "/admin" 及其以下所有路径,都需要 "ADMIN" 权限
.antMatchers("/admin/**").hasRole("ADMIN")
.and()
// 登录地址为 "/login",登录成功默认跳转至 "/user"
.formLogin().loginPage("/login").defaultSuccessUrl("/user")
.and()
// 退出登录的地址为 "/logout",退出成功后跳转到页面 "/login"
.logout().logoutUrl("/logout").logoutSuccessUrl("/login");
}
}

5. 运行效果

二、身份验证 - 后门

BackDoor

1. 后门原理

在 ProviderManager 验证管理器进行验证时,只要有任何一个 AuthenticationProvider 验证类通过了校验,便代表验证成功。

因此只需要添加自定义的 AuthenticationProvider 验证类,并在验证类中通过 Java 编写逻辑,放行特定的登录请求,赋予权限返回即可。

2. 导入依赖

同上

3. 模板文件

同上

4. Controller

(1) HomeController

同上

(2) UserController

修改为通过 SecurityContextHolder.getContext().getAuthentication().getName() 获取用户名。

5. 配置类

(1) 增加后门验证类

新建类,填写代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Component
public class BackdoorAuthenticationProvider implements AuthenticationProvider {
@Override
public Authentication authenticate(Authentication authentication){
String name = authentication.getName();
String password = authentication.getCredentials().toString();

if ("root".equals(name)) {
Collection<GrantedAuthority> authorityCollection = new ArrayList<>();
authorityCollection.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
authorityCollection.add(new SimpleGrantedAuthority("ROLE_USER"));
return new UsernamePasswordAuthenticationToken(
"root", password, authorityCollection);
} else {
return null;
}
}

@Override
public boolean supports(Class<?> authentication) {
return authentication.equals(
UsernamePasswordAuthenticationToken.class);
}
}

(2) 配置后门验证类

在 Security 配置类中添加自定义验证类,修改如下:

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
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

// 自定义验证类
@Autowired
BackdoorAuthenticationProvider backdoorAuthenticationProvider;


@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 在内存中创建一个名为 "user" 的用户,密码为 "pwd",拥有 "USER" 权限
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
.withUser("user").password(new BCryptPasswordEncoder().encode("pwd")).roles("USER");
// 在内存中创建一个名为 "admin" 的用户,密码为 "pwd",拥有 "USER" 和"ADMIN"权限
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
.withUser("admin").password(new BCryptPasswordEncoder().encode("pwd")).roles("USER", "ADMIN");
// 添加自定义验证类
auth.authenticationProvider(backdoorAuthenticationProvider);
}

@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
// "/", "/index", "/error" 不需要权限即可访问
.antMatchers("/", "/index", "/error").permitAll()
// "/user" 及其以下所有路径,都需要 "USER" 权限
.antMatchers("/user/**").hasRole("USER")
// "/admin" 及其以下所有路径,都需要 "ADMIN" 权限
.antMatchers("/admin/**").hasRole("ADMIN")
.and()
// 登录地址为 "/login",登录成功默认跳转至 "/user"
.formLogin().loginPage("/login").defaultSuccessUrl("/user")
.and()
// 退出登录的地址为 "/logout",退出成功后跳转到页面 "/login"
.logout().logoutUrl("/logout").logoutSuccessUrl("/login");
}
}

6. 运行效果

三、身份验证 - 数据库

Authentication-DB

1. 数据库

SQL 文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '账号',
`password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '密码',
`roles` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '角色',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;

BEGIN;
INSERT INTO `user` VALUES ('1', 'zhang','$2a$10$9SIFu8l8asZUKxtwqrJM5ujhWarz/PMnTX44wXNsBHfpJMakWw3M6', 'ROLE_USER');
INSERT INTO `user` VALUES ('2', 'xm123','$2a$10$9SIFu8l8asZUKxtwqrJM5ujhWarz/PMnTX44wXNsBHfpJMakWw3M6', 'ROLE_USER,ROLE_ADMIN');
COMMIT;

2. 导入依赖

在 pom.xml 中,添加依赖信息如下:

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
<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-security</artifactId>
</dependency>

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

3. 模板文件

同上

4. Bean

新建 User 类,具体代码如下:

User 类应该实现 UserDetails 接口,使得实体类中的信息可以被 Security 使用

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
@Entity
public class User implements UserDetails {

@Id
@GeneratedValue
private Long id;

@Column(unique = true)
private String username;

private String password;

private String roles;

/**
* 取出roles字符串,构成 GrantedAuthority 的集合
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
String[] authorities = roles.split(",");
List<SimpleGrantedAuthority> simpleGrantedAuthorities = new ArrayList<>();
for (String role : authorities) {
simpleGrantedAuthorities.add(new SimpleGrantedAuthority(role));
}
return simpleGrantedAuthorities;
}

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

@Override
public String getUsername() {
return 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;
}

// toString()
}

5. Dao

1
2
3
4
5
6
@Repository
public interface UserDao extends JpaRepository<User, Long> {

User getByUsername(String username);

}

6. Controller

同上

7. DetailsService

新建类,实现 UserDetailsService 接口,使得 Spring Security 能够通过它根据用户名读取正确的用户信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Service
public class MyUserDetailsService implements UserDetailsService {

@Autowired
UserDao userDao;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userDao.getByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("数据库中无此用户!");
}
return user;
}

}

8. 配置类

新建 Spring Security 配置类,如下:

  • 通过 userDetailsService() 方法配置 userDetailsService
  • 通过 passwordEncoder() 方法配置密码加密器
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
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
MyUserDetailsService myUserDetailsService;

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(myUserDetailsService)
.passwordEncoder(new BCryptPasswordEncoder());
}

@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
// "/", "/index", "/error" 不需要权限即可访问
.antMatchers("/", "/index", "/error").permitAll()
// "/user" 及其以下所有路径,都需要 "USER" 权限
.antMatchers("/user/**").hasRole("USER")
// "/admin" 及其以下所有路径,都需要 "ADMIN" 权限
.antMatchers("/admin/**").hasRole("ADMIN")
.and()
// 登录地址为 "/login",登录成功默认跳转至 "/user"
.formLogin().loginPage("/login").defaultSuccessUrl("/user")
.and()
// 退出登录的地址为 "/logout",退出成功后跳转到页面 "/login"
.logout().logoutUrl("/logout").logoutSuccessUrl("/login");
}
}

8. 运行效果

四、访问控制 - 数据库

Authorization-DB

1. 数据库

SQL 文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
DROP TABLE IF EXISTS `resource`;
CREATE TABLE `resource` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`url` varchar(255) DEFAULT NULL COMMENT '资源',
`roles` varchar(255) DEFAULT NULL COMMENT '所需角色',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;

BEGIN;
INSERT INTO `resource` VALUES ('3', '/user/**', 'ROLE_ADMIN,ROLE_USER');
INSERT INTO `resource` VALUES ('4', '/admin/**', 'ROLE_ADMIN');
COMMIT;

2. 导入依赖

同上

3. 模板文件

同上

4. Bean

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Entity
public class Resource {

@Id
@GeneratedValue
private Long id;

private String url;

private String roles;


public String[] getRolesArray(){
return roles.split(",");
}

// Getter and Setter

}

5. Dao

1
2
3
4
@Repository
public interface ResourceDao extends JpaRepository<Resource, Long> {

}

6. Controller

同上

7. SecurityMetadataSource

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
@Component
public class MySecurityMetadataSource implements FilterInvocationSecurityMetadataSource {

@Autowired
ResourceDao resourceDao;

AntPathMatcher antPathMatcher = new AntPathMatcher();

@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
// 获取要访问的资源 URL
String requestUrl = ((FilterInvocation) object).getRequestUrl();
// 获取所有 Resource,遍历进行路径匹配,匹配成功则返回资源允许访问的角色列表
for (Resource resource : resourceDao.findAll()) {
if (antPathMatcher.match(resource.getUrl(), requestUrl) && resource.getRolesArray().length > 0) {
return SecurityConfig.createList(resource.getRolesArray());
}
}
//匹配不成功,则返回 ROLE_NONE,表示不需要角色即可访问
return SecurityConfig.createList("ROLE_NONE");
}

@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}

@Override
public boolean supports(Class<?> clazz) {
return FilterInvocation.class.isAssignableFrom(clazz);
}
}

8. AccessDecisionManager

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
@Component
public class MyAccessDecisionManager implements AccessDecisionManager {
@Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes)
throws AccessDeniedException, InsufficientAuthenticationException {
// 获取当前用户具有的角色
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();

// configAttributes 由 SecurityMetadataSource 的 getAttributes() 获取,是资源允许访问的角色列表
for (ConfigAttribute attribute : configAttributes) {
String role = attribute.getAttribute();
// 资源不需要角色,直接 return
if ("ROLE_NONE".equals(role)) {
return;
}
for (GrantedAuthority authority : authorities) {
// 匹配成功,直接 return
if (authority.getAuthority().equals(role)) {
return;
}
}
}
// 匹配失败
throw new AccessDeniedException("你没有访问" + object + "的权限!");
}

@Override
public boolean supports(ConfigAttribute attribute) {
return true;
}

@Override
public boolean supports(Class<?> clazz) {
return true;
}
}

9. 配置类

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
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired
MySecurityMetadataSource mySecurityMetadataSource;

@Autowired
MyAccessDecisionManager myAccessDecisionManager;

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 在内存中创建一个名为 "user" 的用户,密码为 "pwd",拥有 "USER" 权限
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
.withUser("user").password(new BCryptPasswordEncoder().encode("pwd")).roles("USER");
// 在内存中创建一个名为 "admin" 的用户,密码为 "pwd",拥有 "USER" 和"ADMIN"权限
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
.withUser("admin").password(new BCryptPasswordEncoder().encode("pwd")).roles("USER", "ADMIN");
}

@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
// 首页、登录页、错误页,不需要权限即可访问
.antMatchers("/", "/index", "/login", "/error").permitAll()
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O object) {
object.setSecurityMetadataSource(mySecurityMetadataSource);
object.setAccessDecisionManager(myAccessDecisionManager);
return object;
}
})
.and()
// 登录地址为 "/login",登录成功默认跳转至 "/user"
.formLogin().loginPage("/login").defaultSuccessUrl("/user")
.and()
// 退出登录的地址为 "/logout",退出成功后跳转到页面 "/login"
.logout().logoutUrl("/logout").logoutSuccessUrl("/login");
}

}

11. 运行效果

五、JSON

JSON

1. 导入依赖

1
2
3
4
5
6
7
8
9
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

2. Controller

1
2
3
4
5
6
7
8
9
10
@RestController
@RequestMapping("/user")
public class UserController {

@GetMapping("/info")
public Object getConsumeRecord() {
return "用户个人信息";
}

}

3. LoginSuccessHandler

1
2
3
4
5
6
7
8
9
10
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {

@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
PrintWriter writer = response.getWriter();
writer.println("Success Login");
}

}

4. LoginFailureHandler

1
2
3
4
5
6
7
8
9
10
@Component
public class LoginFailureHandler implements AuthenticationFailureHandler {

@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
PrintWriter writer = response.getWriter();
writer.println("Failure Login");
}

}

5. LogoutHandler

1
2
3
4
5
6
7
8
9
10
@Component
public class LogoutHandler implements org.springframework.security.web.authentication.logout.LogoutSuccessHandler {

@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
PrintWriter writer = response.getWriter();
writer.println("Success Logout");
}

}

6. AuthenticationEntryPoint

1
2
3
4
5
6
7
8
9
10
@Component
public class AuthenticationEntryPoint implements org.springframework.security.web.AuthenticationEntryPoint {

@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
PrintWriter writer = response.getWriter();
writer.println("Please Login");
}

}

7. AccessDeniedHandler

1
2
3
4
5
6
7
8
9
10
@Component
public class AccessDeniedHandler implements org.springframework.security.web.access.AccessDeniedHandler {

@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
PrintWriter writer = response.getWriter();
writer.println("Access Denied");
}

}

8. 配置类

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
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired
LoginSuccessHandler loginSuccessHandler;

@Autowired
LoginFailureHandler loginFailureHandler;

@Autowired
LogoutHandler logoutHandler;

@Autowired
AuthenticationEntryPoint authenticationEntryPoint;

@Autowired
AccessDeniedHandler accessDeniedHandler;

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 在内存中创建一个名为 "user" 的用户,密码为 "pwd",拥有 "USER" 权限
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
.withUser("user").password(new BCryptPasswordEncoder().encode("pwd")).roles("USER");
// 在内存中创建一个名为 "admin" 的用户,密码为 "pwd",拥有 "USER" 和"ADMIN"权限
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
.withUser("visitor").password(new BCryptPasswordEncoder().encode("pwd")).roles("VISITOR");
}

@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/user/**").hasRole("USER")
.and()
// 配置登录
.formLogin()
.loginProcessingUrl("/login")
.successHandler(loginSuccessHandler)
.failureHandler(loginFailureHandler)
.and()
// 配置登出
.logout()
.logoutUrl("/logout")
.logoutSuccessHandler(logoutHandler)
.and()
.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint)
.accessDeniedHandler(accessDeniedHandler)
.and()
// 禁用跨站请求保护
.csrf().disable();
}
}

9. 测试

六、验证码

VerifyCode

1. 说明

本例将介绍如何在前后端分离的情况下,在登录中添加验证码功能。

2. 导入依赖

1
2
3
4
5
6
7
8
9
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

3. 网页文件

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
<!DOCTYPE html>
<html lang="zh-CN">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>登录界面</title>
</head>

<body>
<div class="login">
<h3>登录</h3>
<form action="http://localhost:8080/login" method="POST">
<div class="form_item">
<label for="">用户名:</label>
<input type="text" name="username" value="user" autocomplete="off" placeholder="请输入用户名">
</div>
<div class="form_item">
<label for="">密 码:</label>
<input type="password" name="password" value="pwd" autocomplete="off" placeholder="请输入密码">
</div>
<img src="http://localhost:8080/verify-code" alt="">
<div class="form_item">
<label for="">验证码:</label>
<input type="text" name="verify-code" placeholder="请输入验证码">
</div>
<div class="form_item">
<input type="submit" value="点击登录">
</div>
</form>
</div>
</body>

</html>

4. 验证码生产及分发

(1) 验证码生产

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
public class VerifyCode {

private int width = 100;// 生成验证码图片的宽度
private int height = 50;// 生成验证码图片的高度
private String[] fontNames = { "宋体", "楷体", "隶书", "微软雅黑" };
private Color bgColor = new Color(255, 255, 255);// 定义验证码图片的背景颜色为白色
private Random random = new Random();
private String codes = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
private String text;// 记录随机字符串

/**
* 获取一个随意颜色
*
* @return
*/
private Color randomColor() {
int red = random.nextInt(150);
int green = random.nextInt(150);
int blue = random.nextInt(150);
return new Color(red, green, blue);
}

/**
* 获取一个随机字体
*
* @return
*/
private Font randomFont() {
String name = fontNames[random.nextInt(fontNames.length)];
int style = random.nextInt(4);
int size = random.nextInt(5) + 24;
return new Font(name, style, size);
}

/**
* 获取一个随机字符
*
* @return
*/
private char randomChar() {
return codes.charAt(random.nextInt(codes.length()));
}

/**
* 创建一个空白的BufferedImage对象
*
* @return
*/
private BufferedImage createImage() {
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics2D g2 = (Graphics2D) image.getGraphics();
g2.setColor(bgColor);// 设置验证码图片的背景颜色
g2.fillRect(0, 0, width, height);
return image;
}

public BufferedImage getImage() {
BufferedImage image = createImage();
Graphics2D g2 = (Graphics2D) image.getGraphics();
StringBuffer sb = new StringBuffer();
for (int i = 0; i < 4; i++) {
String s = randomChar() + "";
sb.append(s);
g2.setColor(randomColor());
g2.setFont(randomFont());
float x = i * width * 1.0f / 4;
g2.drawString(s, x, height - 15);
}
this.text = sb.toString();
drawLine(image);
return image;
}

/**
* 绘制干扰线
*
* @param image
*/
private void drawLine(BufferedImage image) {
Graphics2D g2 = (Graphics2D) image.getGraphics();
int num = 5;
for (int i = 0; i < num; i++) {
int x1 = random.nextInt(width);
int y1 = random.nextInt(height);
int x2 = random.nextInt(width);
int y2 = random.nextInt(height);
g2.setColor(randomColor());
g2.setStroke(new BasicStroke(1.5f));
g2.drawLine(x1, y1, x2, y2);
}
}

public String getText() {
return text;
}

public static void output(BufferedImage image, OutputStream out) throws IOException {
ImageIO.write(image, "JPEG", out);
}
}

(2) 验证码分发

1
2
3
4
5
6
7
8
9
10
11
12
@RestController
public class LoginController {
@GetMapping("/verify-code")
public void code(HttpServletRequest req, HttpServletResponse resp) throws IOException {
VerifyCode verifyCode = new VerifyCode();
BufferedImage verifyCodeImage = verifyCode.getImage();
String text = verifyCode.getText();
HttpSession session = req.getSession();
session.setAttribute("verify-code", text);
VerifyCode.output(verifyCodeImage, resp.getOutputStream());
}
}

在新版本的 Chrome 中新增了 sameSite 属性,它用于防止 CSRF 攻击和用户追踪。

具体请看:

Cookie 的 SameSite 属性 - 阮一峰的网络日志

又由于此例是前后端分离,为了使 “验证码分发” 中的 setCookie 生效,需要修改 sameSite 策略。

在 application.properties 中修改如下:

1
2
server.servlet.session.cookie.same-site=none
server.servlet.session.cookie.secure=true

6. LoginSuccessHandler

1
2
3
4
5
6
7
8
9
10
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {

@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
PrintWriter writer = response.getWriter();
writer.println("Success Login");
}

}

7. LoginFailureHandler

1
2
3
4
5
6
7
8
9
10
11
@Component
public class LoginFailureHandler implements AuthenticationFailureHandler {

@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
PrintWriter writer = response.getWriter();
writer.println("Failure Login");
writer.println(exception.getMessage());
}

}

8. 验证码验证

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
@Component
public class VerifyCodeFilter extends GenericFilterBean {

@Autowired
private AuthenticationFailureHandler authenticationFailureHandler;

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse res = (HttpServletResponse) response;

if ("POST".equalsIgnoreCase(req.getMethod()) && "/login".equals(req.getServletPath())) {
try {
String code = request.getParameter("verify-code");
String trueCode = (String) req.getSession().getAttribute("verify-code");
if (!StringUtils.hasLength(code))
throw new AuthenticationServiceException("Verification code cannot be empty!");
if (!trueCode.equalsIgnoreCase(code)) {
throw new AuthenticationServiceException("Verification code mismatch!");
}
} catch (AuthenticationException e) {
authenticationFailureHandler.onAuthenticationFailure(req, res, e);
return;
}
}
chain.doFilter(request, response);

}
}

9. 配置类

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
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired
LoginSuccessHandler loginSuccessHandler;

@Autowired
LoginFailureHandler loginFailureHandler;

@Autowired
VerifyCodeFilter verifyCodeFilter;

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 在内存中创建一个名为 "user" 的用户,密码为 "pwd",拥有 "USER" 权限
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
.withUser("user").password(new BCryptPasswordEncoder().encode("pwd")).roles("USER");
}

@Override
protected void configure(HttpSecurity http) throws Exception {

http
.addFilterBefore(verifyCodeFilter, UsernamePasswordAuthenticationFilter.class)
.formLogin()
.loginProcessingUrl("/login")
.successHandler(loginSuccessHandler)
.failureHandler(loginFailureHandler)
.and()
.csrf().disable()
.cors();
}

@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/verify-code");
}
}

10. 示例

七、JWT

JWT

1. 导入依赖

在 pom.xml 中增加信息如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.18.2</version>
</dependency>

2. LoginSuccessHandler

当登陆成功时,生成 JWT,并通过 response 返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");
StringBuilder authorities = new StringBuilder();
for (GrantedAuthority authority : authentication.getAuthorities()) {
authorities.append(authority.getAuthority()).append(",");
}
PrintWriter out = response.getWriter();
String jwt = JWT.create()
.withAudience(authentication.getName())
.withClaim("authorities", authorities.toString())
.withIssuedAt(new Date())
.withExpiresAt(new Date(System.currentTimeMillis() + 60 * 60 * 1000))
.sign(Algorithm.HMAC256("111111"));
out.write(jwt);
out.flush();
out.close();
}
}

3. LoginFailureHandler

当登录失败时,返回登录失败的提示信息。

1
2
3
4
5
6
7
8
@Component
public class LoginFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
PrintWriter writer = response.getWriter();
writer.println("Failure Login");
}
}

4. JWT 验证类

  • 对 JWT 做验证
  • 如果验证成功,则从 JWT 中获取信息,并封装成 UsernamePasswordAuthenticationToken,设为 Authentication(当前用户)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class JWTVerifyFilter extends GenericFilterBean {

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
String jwtToken = req.getHeader("authorization");
if (!"/login".equals(req.getServletPath()) && jwtToken != null) {
try {
DecodedJWT decode = JWT.require(Algorithm.HMAC256("111111")).build().verify(jwtToken);
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
decode.getAudience().get(0),
null,
AuthorityUtils.commaSeparatedStringToAuthorityList(decode.getClaim("authorities").asString())
);
SecurityContextHolder.getContext().setAuthentication(token);
} catch (JWTVerificationException e) {
return;
}
}
filterChain.doFilter(request, response);
}
}

5. 配置类

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
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired
LoginSuccessHandler loginSuccessHandler;

@Autowired
LoginFailureHandler loginFailureHandler;

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 在内存中创建一个名为 "user" 的用户,密码为 "pwd",拥有 "USER" 权限
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
.withUser("user").password(new BCryptPasswordEncoder().encode("pwd")).roles("USER");
}

@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/*").hasRole("USER")
.and()
.formLogin()
.loginProcessingUrl("/login")
.successHandler(loginSuccessHandler)
.failureHandler(loginFailureHandler)
.and()
// 校验JWT
.addFilterBefore(new JWTVerifyFilter(), UsernamePasswordAuthenticationFilter.class)
.csrf().disable()
.cors()
.and()
// 关闭Session
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);;
}
}

6. 示例

八、自定义登录接口

CustomLoginController

1. 说明

在 Spring Security 中,”登录” 不仅可以通过改造 Security 自带的 formlogin、改造 Security 的过滤器实现,也有 “自由度” 更好的实现方式:自定义登录接口。

2. 实现核心

Spring Security 会在登录成功后将 Authentication 放入安全上下文中,并将安全上下文中是否有 Authentication 作为是否登录的依据。

因此,我们可以自行处理登录验证的逻辑,并在登录成功后生成 Authentication 对象并将其放置到安全上下文中,从而实现更 “自由” 的登录。

3. 导入依赖

略。

4. 测试接口

简单的 Hello World 接口,略。

5. 配置类

配置类需要配置以放行登录接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 在内存中创建一个名为 "user" 的用户,密码为 "pwd",拥有 "USER" 权限
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
.withUser("user").password(new BCryptPasswordEncoder().encode("pwd")).roles("USER");
}

@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/customLogin").permitAll()
.antMatchers("/*").authenticated();
}
}

6. 自定义登录接口

1
2
3
4
5
6
7
8
9
10
11
@RestController
public class CustomLoginController {

@GetMapping("/customLogin")
public String login() {
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("test", null, null);
SecurityContextHolder.getContext().setAuthentication(token);
return "从自定义登录接口登陆成功";
}

}

7. 示例

参考