0%

Spring Boot2 实战系列之登录注册(二) - 登录实现

前言

在前面的博文 Spring Boot2 实战系列之登录注册(一) - 注册实现 中实现了一个基本的注册功能,这次继续把登录功能加上,采用 spring security 对用户进行认证,采用 session 管理用户登录状态。

项目架构

项目结构图如下:

pom 依赖如下:

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
<?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.5.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>top.yekongle</groupId>
<artifactId>springboot-login-sample</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-boot-starter-parent</name>
<description>Login sample for Spring Boot</description>

<properties>
<java.version>1.8</java.version>
<passay.version>1.5.0</passay.version>
<guava.version>29.0-jre</guava.version>
</properties>

<dependencies>
<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>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.passay</groupId>
<artifactId>passay</artifactId>
<version>${passay.version}</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</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>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

代码编写

这里主要写出改动或新增的类,其他的则和注册实现篇基本一致

用户角色
UserAuthority.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
package top.yekongle.login.entity;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

import lombok.Data;
import lombok.NoArgsConstructor;

/**
* @Description:
* @Author: Yekongle
* @Date: 2020年5月14日
*/

@Entity
@Data
@NoArgsConstructor
public class UserAuthority {
@Id
@GeneratedValue
private Long id;
private String username;
private String role;
}

用户角色操作接口
UserAuthorityRepository.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package top.yekongle.login.repository;

import java.util.List;

import org.springframework.data.jpa.repository.JpaRepository;

import top.yekongle.login.entity.UserAuthority;

/**
* @Description:
* @Author: Yekongle
* @Date: 2020年5月14日
*/
public interface UserAuthorityRepository extends JpaRepository<UserAuthority, Long> {
List<UserAuthority> findByUsername(String username);
}

注册方法,注册用户时默认指定一个 “ROLE_USER” 角色并保存
UserServiceImpl.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
package top.yekongle.login.service.impl;

import java.util.Arrays;

import javax.transaction.Transactional;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import lombok.extern.slf4j.Slf4j;
import top.yekongle.login.dto.UserDTO;
import top.yekongle.login.entity.User;
import top.yekongle.login.entity.UserAuthority;
import top.yekongle.login.exception.UserAlreadyExistException;
import top.yekongle.login.repository.UserAuthorityRepository;
import top.yekongle.login.repository.UserRepository;
import top.yekongle.login.service.UserService;

/**
* @Description:
* @Author: Yekongle
* @Date: 2020年5月5日
*/
@Slf4j
@Service
public class UserServiceImpl implements UserService {

@Autowired
private UserRepository userRepository;

@Autowired
private UserAuthorityRepository userAuthorityRepository;

@Autowired
private PasswordEncoder passwordEncoder;

@Transactional
@Override
public User registerNewUserAccount(UserDTO userDTO) throws UserAlreadyExistException {
if (emailExists(userDTO.getEmail())) {
throw new UserAlreadyExistException("该邮箱已被注册:" + userDTO.getEmail());
}
log.info("UserDTO:" + userDTO.toString());
User user = new User();
user.setEmail(userDTO.getEmail());
user.setPassword(passwordEncoder.encode(userDTO.getPassword()));
userRepository.save(user);

UserAuthority userAuthority = new UserAuthority();
userAuthority.setUsername(userDTO.getEmail());
userAuthority.setRole("ROLE_USER");
userAuthorityRepository.save(userAuthority);

return user;
}

private boolean emailExists(String email) {
return userRepository.findByEmail(email) != null;
}
}

web mvc 配置,这里主要指定直接返回的页面
WebMvcConfig.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
package top.yekongle.login.config;

import java.util.Locale;

import org.springframework.web.servlet.LocaleResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.i18n.CookieLocaleResolver;

/**
* @Description: web mvc 配置
* @Author: Yekongle
* @Date: 2020年5月8日
*/
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {


@Override
public void addViewControllers(ViewControllerRegistry registry) {
/*
* 设置对"/"的请求映射到login, 如果没有逻辑业务,
* 则没有必要用控制器方法对请求进行映射
* */
registry.addViewController("/").setViewName("forward:/login");
registry.addViewController("/registration.html");
registry.addViewController("/successRegister.html");
registry.addViewController("/home.html");
registry.addViewController("/logout.html");
registry.addViewController("/invalidSession.html");
}
}

web 安全配置,主要包括自定义用户认证,登入登出配置,访问控制,session 管理等。
WebSecurityConfig.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
	package top.yekongle.login.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
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.session.SessionRegistry;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;

import org.springframework.security.web.session.HttpSessionEventPublisher;
import top.yekongle.login.security.MyLogoutSuccessHandler;
import top.yekongle.login.security.MyUserDetailServiceImpl;

/**
* @Description: Web 安全配置
* @Author: Yekongle
* @Date: 2020年5月5日
*/
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired
private MyUserDetailServiceImpl userDetailsService;

@Autowired
private AuthenticationSuccessHandler authenticationSuccessHandler;

@Autowired
private AuthenticationFailureHandler authenticationFailureHandler;

@Autowired
private MyLogoutSuccessHandler myLogoutSuccessHandler;

// 使用 BCrypt强哈希方法来加密密码, 每次加密结果不一样
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}


/*
* 可以有多个 AuthenticationProvider,默认使用 DaoAuthenticationProvide
* DaoAuthenticationProvider 在进行认证的时候需要一个 UserDetailsService 来获取用户的信息 UserDetails
* 其中包括用户名、密码和所拥有的权限
* 当其中一个 AuthenticationProvider 认证成功后,后续 provider不再认证
* */
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
authenticationProvider.setUserDetailsService(userDetailsService);
authenticationProvider.setPasswordEncoder(passwordEncoder());
return authenticationProvider;
}

/* 注册 session 创建和销毁监听器,以便用于支持 session 并发控制
* 通知 Spring Security 更新会话注册表
* 实际上创建的监听只使用销毁事件
**/
@Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}

// 跟踪活跃的session
@Bean
public SessionRegistry sessionRegistry() {
return new SessionRegistryImpl();
}

/*
* 用户认证
* 这里使用通用的用户认证,还有基于内存的用户和JDBC中的用户
* 数据访问方式可以是多种多样,包括非关系型数据库, 这时就需先自定义实现 UserDetailsService 接口来获取用户信息
* */
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationProvider());
}

/*
* 请求授权配置
* */
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
// 允许访问 H2 DB 控制台
.antMatchers("/h2/**").permitAll()
.antMatchers("/css/**", "/js/**", "/fonts/**").permitAll()
.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
.antMatchers("/user/registration*", "/registration*", "/successRegister*", "/login*", "/logout*").permitAll()
.antMatchers("/invalidSession*").anonymous()
.anyRequest().authenticated()
.and()
.formLogin().loginPage("/login")
// 使用自定义登录成功处理
.successHandler(authenticationSuccessHandler)
// 使用自定义登录成功处理
.failureHandler(authenticationFailureHandler)
.permitAll()
.and()
.sessionManagement()
// 无效 session 跳转
.invalidSessionUrl("/invalidSession.html")
// 确保单个用户的单个账号,只有一个活跃的session
.maximumSessions(1).sessionRegistry(sessionRegistry()).and()
// 创建一个新的HTTP会话后,使旧的HTTP会话无效,并将旧会话的属性复制过来
.sessionFixation().migrateSession()
.and()
.logout()
// 使用自定义注销登录成功处理
.logoutSuccessHandler(myLogoutSuccessHandler)
// 会清空所有已定义的session
.invalidateHttpSession(false)
// 删除 cookie
.deleteCookies("JSESSIONID")
.permitAll();
}

}

实现用户信息接口,自定义获取用户信息的方法,主要时实现了 loadUserByUsername 方法,并返回一个封装了用户账号,密码,权限等信息的 UserDetails 类型的实例 User。
MyUserDetailServiceImpl.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
package top.yekongle.login.security;

import java.util.ArrayList;
import java.util.List;

import javax.transaction.Transactional;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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 lombok.extern.slf4j.Slf4j;
import top.yekongle.login.repository.UserAuthorityRepository;
import top.yekongle.login.repository.UserRepository;
import top.yekongle.login.entity.User;
import top.yekongle.login.entity.UserAuthority;
/**
* @Description:
* @Author: Yekongle
* @Date: 2020年5月5日
*/

@Slf4j
@Service("userDetailsService")
@Transactional
public class MyUserDetailServiceImpl implements UserDetailsService {

@Autowired
private UserRepository userRepository;

@Autowired
private UserAuthorityRepository userAuthorityRepository;

@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
log.info("Email:" + email);
User user = userRepository.findByEmail(email);

if (user == null) {
throw new UsernameNotFoundException("找不到该用户: "+ email);
}
boolean enabled = true;
boolean accountNonExpired = true;
boolean credentialsNonExpired = true;
boolean accountNonLocked = true;

return new org.springframework.security.core.userdetails
.User(user.getEmail(), user.getPassword(), enabled, accountNonExpired
, credentialsNonExpired, accountNonLocked, getAuthorities(user.getEmail()));
}

private List<GrantedAuthority> getAuthorities (String username) {
List<GrantedAuthority> authorities = new ArrayList<>();
List<UserAuthority> userAuthorityList = userAuthorityRepository.findByUsername(username);
log.info("role size:" + userAuthorityList.size());
for (UserAuthority userAuthority : userAuthorityList) {
authorities.add(new SimpleGrantedAuthority(userAuthority.getRole()));
}
return authorities;
}
}

自定义登录成功处理器,这里主要是设置会话有效期和指定重定向页面
CustomAuthenticationSuccessHandler.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
package top.yekongle.login.security;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.WebAttributes;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import lombok.extern.slf4j.Slf4j;

/**
* @Author: Yekongle
* @Date: 2020年5月13日
*/
@Slf4j
@Component("authenticationSuccessHandler")
public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
log.info("onAuthenticationSuccess");
redirectStrategy.sendRedirect(request, response, "/home.html");

// 获取session,如果 session不存在,则返回null。
final HttpSession session = request.getSession(false);
if (session != null) {
// session 有效期 30 min
session.setMaxInactiveInterval(30*60);
String username = this.getCurrentUsername(authentication);
session.setAttribute("user", username);
}
// 清除 session 中的 AUTHENTICATION_EXCEPTION 属性
clearAuthenticationAttributes(request);
}


private String getCurrentUsername(Authentication authentication) {
String username = null;
if (authentication.getPrincipal() instanceof UserDetails) {
username = ((UserDetails) authentication.getPrincipal()).getUsername();
} else {
username = authentication.getName();
}
return username;
}

protected void clearAuthenticationAttributes(final HttpServletRequest request) {
final HttpSession session = request.getSession(false);
if (session == null) {
return;
}
session.removeAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
}

}

自定义登录失败处理器,控制跳转,返回错误信息
CustomAuthenticationFailureHandler.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
package top.yekongle.login.security;

import java.io.IOException;
import java.util.Locale;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.WebAttributes;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.LocaleResolver;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Component("authenticationFailureHandler")
public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {

@Override
public void onAuthenticationFailure(final HttpServletRequest request, final HttpServletResponse response, final AuthenticationException exception) throws IOException, ServletException {
log.info("onAuthenticationFailure");
setDefaultFailureUrl("/login?error=true");

super.onAuthenticationFailure(request, response, exception);

request.getSession()
.setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, exception.getMessage());
}
}

注销登录成功处理
MyLogoutSuccessHandler.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
package top.yekongle.login.security;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.stereotype.Component;

/**
* @Author: Yekongle
* @Date: 2020年5月13日
*/

@Component("myLogoutSuccessHandler")
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {

@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
final HttpSession session = request.getSession();
if (session != null) {
// 清理自定义 session 属性信息
session.removeAttribute("user");
}

response.sendRedirect("/logout.html?logSucc=true");
}

}

登录请求处理
LoginController.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
package top.yekongle.login.controller;

import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

/**
* @Author: Yekongle
* @Date: 2020年5月5日
*/

@Controller
public class LoginController {

@GetMapping("/login")
public String login() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
// 如果已经登录则跳转到 home 页面
if (auth instanceof AnonymousAuthenticationToken) {
return "login";
} else {
return "home";
}
}
}

登录页面
loign.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
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
<html xmlns:th="http://www.thymeleaf.org"><!-- Thymeleaf的命名空间,将静态页面转换为动态的视图 -->
<head>
<meta content="text/html;charset=UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<link th:href="@{/css/bootstrap.min.css}" rel="stylesheet"/>

<style type="text/css">
.middle {
float: none;
display: inline-block;
vertical-align: middle;
}
</style>
</head>
<body>
<div th:if="${param.error != null}" class="alert alert-danger" th:utext="${session[SPRING_SECURITY_LAST_EXCEPTION]}">error</div>
<div class="container">
<h2>登录</h2>
<br/>
<form name='loginForm' action="login" method="POST" onsubmit="return validate();">
<div class="row">
<div class="form-group col-md-6 vertical-middle-sm">
<label for="email">邮箱</label>
<input type="email" class="form-control" name="username" aria-describedby="emailHelp">
</div>
</div>

<div class="row">
<div class="form-group col-md-6">
<label for="password">密码</label>
<input type="password" class="form-control" id="password" name="password">
</div>
</div>
<button type="submit" class="btn btn-primary">登录</button>
<a class="btn btn-default" th:href="@{/registration.html}" >没有账号?</a>
</form>
</div>

<script th:src="@{/js/jquery-3.5.1.min.js}" type="text/javascript"></script>
<script th:src="@{/js/bootstrap.min.js}" type="text/javascript"></script>
<script th:inline="javascript">
function validate() {
if (document.loginForm.username.value == "" && document.loginForm.password.value == "") {
alert("账号密码不能为空!");
document.loginForm.username.focus();
return false;
}
if (document.loginForm.username.value == "") {
alert("账号不能为空!");
document.loginForm.username.focus();
return false;
}
if (document.loginForm.password.value == "") {
alert("密码不能为空!");
document.loginForm.password.focus();
return false;
}
}
</script>

</body>
</html>

主页
home.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
<html xmlns:th="http://www.thymeleaf.org"><!-- Thymeleaf的命名空间,将静态页面转换为动态的视图 -->
<head>
<meta content="text/html;charset=UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<link th:href="@{/css/bootstrap.min.css}" rel="stylesheet"/>
</head>
<body>

<nav class="navbar navbar-default">
<div class="container-fluid">
<div class="navbar-header">
<a class="navbar-brand" href="#">home</a>
</div>
<ul class="nav navbar-nav navbar-right">
<li><a style="color:blue;" th:text="${session[user]}" >yekongle</a></li>
<li><a style="color:orange;" th:href="@{/logout}" >logout</a></li>
</ul>
</div>
</nav>

<script th:src="@{/js/jquery-3.5.1.min.js}" type="text/javascript"></script>
<script th:src="@{/js/bootstrap.min.js}" type="text/javascript"></script>
<script th:inline="javascript">

</script>

</body>
</html>

注销登录结果页面
logout.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<html xmlns:th="http://www.thymeleaf.org"><!-- Thymeleaf的命名空间,将静态页面转换为动态的视图 -->
<head>
<meta content="text/html;charset=UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<link th:href="@{/css/bootstrap.min.css}" rel="stylesheet"/>
</head>

<body>
<div class="container">
<h1 id="error" class="alert alert-danger" th:if="${session[SPRING_SECURITY_LAST_EXCEPTION]}" >退出登录失败</h1>

<h1 id="success" class="alert alert-info" th:if="${param.logSucc}" >退出登录成功</h1>
<br/><br/><br/>
<a class="btn btn-primary" th:href="@{/login}" >登录</a>
</div>
</body>

</html>

无效 session 页面
invalidSession.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<html xmlns:th="http://www.thymeleaf.org"><!-- Thymeleaf的命名空间,将静态页面转换为动态的视图 -->
<head>
<meta content="text/html;charset=UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<link th:href="@{/css/bootstrap.min.css}" rel="stylesheet"/>
</head>
<body>
<div class="container">
<h1 class="alert alert-danger" >登录过期,请重新登录</h1>
<a class="btn btn-primary" th:href="@{/login}" >重新登录</a>
</div>

<script th:src="@{/js/jquery-3.5.1.min.js}" type="text/javascript"></script>
<script th:src="@{/js/bootstrap.min.js}" type="text/javascript"></script>
</body>

</html>

运行演示

启动项目

  1. 访问 http://localhost:8080,会自动跳到登录页面,先点击跳到注册页面

  1. 注册账号,邮箱:test@gmail.com 密码:A123456!

  2. 注册成功,立即登录

  1. 输入刚刚注册的邮箱和密码

  2. 登录成功,跳转到主页,右侧可以显示返回了用户的邮箱

  1. 如果超过了设定的会话有效期 30 min 没有操作行为,则会过期
  1. 点击 logout,注销成功

项目已上传至 Github: https://github.com/yekongle/springboot-code-samples/tree/master/springboot-login-sample , 希望对小伙伴们有帮助哦。

参考链接:

坚持原创技术分享,您的支持将鼓励我继续创作!