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

前言

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

项目架构

项目结构图如下:

在这里插入图片描述

pom 依赖如下:

<?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

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

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

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

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

	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

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

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

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

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

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

<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

<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

<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

<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. 注册成功,立即登录
    在这里插入图片描述

  3. 输入刚刚注册的邮箱和密码
    在这里插入图片描述

  4. 登录成功,跳转到主页,右侧可以显示返回了用户的邮箱
    在这里插入图片描述

  5. 如果超过了设定的会话有效期 30 min 没有操作行为,则会过期
    在这里插入图片描述

  6. 点击 logout,注销成功
    在这里插入图片描述

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

参考链接:

  • https://v4.bootcss.com/docs/getting-started/introduction/
  • https://github.com/Baeldung/spring-security-registration
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值