Spring Security 是基于 Spring 应用的框架,具有功能强大且高度可定制的身份验证和访问控制的特点。

拆箱即用

直接在 pom.xml 文件中增加 spring-boot-starter-security 依赖即可,如下所示:

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

    <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-thymeleaf</artifactId>
            <!-- <version>2.1.3.RELEASE</version> -->
        </dependency>
    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>2.1.16.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
</project>

直接启动应用,控制台会显示其密码,如下所示:

Using generated security password: be84abd1-46a4-49c2-b815-5db9c46f7007

所以,其账号就是 user ,密码就是控制台中输出的内容: be84abd1-46a4-49c2-b815-5db9c46f7007

在配置文件中声明账号和密码

在引入依赖后,可以直接将账号和密码进行自定义配置,如下所示:

spring:
  security:
    user:
      name: nano
      password: nano

所以,其账号就是 nano ,密码也是: nano


在配置类中声明账号和密码

@Configuration
public class SecurityConfigure extends WebSecurityConfigurerAdapter {

	@Bean
	public PasswordEncoder passwordEncoder() {
		return new BCryptPasswordEncoder();
	}

	@Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
		// 将密码进行加密处理
		String pwd = passwordEncoder().encode("coco");
		auth.inMemoryAuthentication()
				// 设置登录账号
				.withUser("coco")
				// 设置登录密码
				.password(pwd).roles("管理员");
	}

}

所以,其账号就是 coco ,密码也是: coco

需要注意的是,如果同时存在在配置文件和配置类对账号和密码进行设置,则配置文件中的配置将会失效!


匹配数据库中的账号和密码

如果需要匹配数据库中的账号和密码,需要借助实现 UserDetailsService 接口来完成。

  1. 首先,准备一个数据表,如下:
CREATE TABLE `security_user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(20) NOT NULL COMMENT '登录账号',
  `password` varchar(32) NOT NULL COMMENT '登录密码',
  PRIMARY KEY (`id`),
  UNIQUE KEY `ix_username` (`username`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;

INSERT INTO `security_user` (`id`, `username`, `password`) VALUES (1, 'root', 'root');
  1. 引入相关的依赖
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.47</version>
</dependency>
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.2.0</version>
</dependency>
  1. 修改配置文件,指定数据库连接信息
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/test?useSSL=false&serverTimeZone=GTM%2B8
    driver-class-name: com.mysql.jdbc.Driver
    username: root
    password: root
  1. 编写查询密码的 Mapper 接口
@Mapper
public interface UserMapper {
	/**
	 * 根据账号查询密码
	 * @param username 账号
	 */
	@Select("SELECT `password` FROM `security_user` WHERE `username` = #{0} LIMIT 1")
	String selectPwdByUsername(String username);
}
  1. 编写 UserDetailsService 接口的实现类
@Service
public class UserService implements UserDetailsService {

	@Resource
	private UserMapper mapper;

	@Resource
	private PasswordEncoder encoder;

	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		// 根据输入的账号查询数据库中的密码
		String pwd = mapper.selectPwdByUsername(username);
		if (StringUtils.isEmpty(pwd)) {
			throw new UsernameNotFoundException("用户名不存在");
		}
		// 将数据库中的明文秘密加密处理
		pwd = encoder.encode(pwd);
		// 构造授权集合对象
		List<GrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList("管理员");
		// 返回登录用户的对象,来自 org.springframework.security.core.userdetails.User
		return new User(username, pwd, authorities);
	}
}
  1. 修改 Security 的配置类
@Configuration
public class SecurityConfigure extends WebSecurityConfigurerAdapter {

	@Resource
	private UserService service;

	@Bean
	public PasswordEncoder passwordEncoder() {
		return new BCryptPasswordEncoder();
	}

	@Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
		auth.userDetailsService(service);
	}

}

所以,其登录账号和密码就是数据表 security_userusername 字段和 password 字段的值来决定的。


自定义登录&注销页面

  1. 修改 Security 的配置类
@Configuration
public class SecurityConfigure extends WebSecurityConfigurerAdapter {

	@Resource
	private UserService service;

	@Bean
	public PasswordEncoder passwordEncoder() {
		return new BCryptPasswordEncoder();
	}

	@Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
		auth.userDetailsService(service);
	}

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http.logout()
				// 自定义注销页面的路由
				.logoutUrl("/security/logout").permitAll();
	
		http.formLogin()
				// 自定义登录页面的地址
				.loginPage("/security/login")
				// 自定义登录处理地址,Security 会自定处理,不需要编写对应的控制器
				// 该地址可以不用指定,默认和 loginPage 是一致的
				.loginProcessingUrl("/security/login-processor")
				.permitAll();

		http.authorizeRequests()
				// 特定地址不需要认证,直接允许被访问
				.antMatchers("/favicon.ico", "/assets/**").permitAll();

		http.authorizeRequests()
				// 任何请求,都需要认证
				.anyRequest().authenticated();
	}
}
  1. 编写登录页面对应的控制器
@Controller
@RequestMapping("security")
public class SecurityController {
	/**
	 * 自定义登录页面
	 */
	@GetMapping("login")
	public String login() {
		return "login";
	}

	/**
	 * 自定义注销页面
	 */
	@GetMapping("logout")
	public String logout(HttpServletRequest request) throws ServletException {
		request.logout();
		return "redirect:/";
	}
}
  1. 编写控制器对应的 ThymeLeaf 模板页面

需要注意的是,账号输入框的 name 必须为 username,密码输入框的 name 必须为 password

<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org">

<head>
    <meta charset="utf-8">
    <title>用户登录</title>
    <link th:href="@{/assets/css/login.css}" rel="stylesheet">
</head>

<body>
<div class="login-container">
    <h3>用户登录</h3>
    <form method="post" th:action="@{/security/login-processor }" autocomplete="off">
        <div class="form-item">
            <input type="text" name="username" placeholder="请输入登录账号"/>
        </div>
        <div class="form-item">
            <input type="password" name="password" placeholder="请输入登录密码"/>
        </div>
        <div class="form-item">
            <button type="submit">登录</button>
        </div>
    </form>
    <div class="login-footer"> springboot security test </div>
</div>
</body>
</html>

到此,便完成了 Security 登录页和注销页的自定义啦。


Ajax 提交账号和密码

Security 在认证完成后,成功会默认执行 successHandler 中指定的 onAuthenticationSuccess 方法,失败会默认执行 failureHandler 中指定的 onAuthenticationFailure 方法,可通过重写该方法实现对异步认证的处理

  1. 编写 SecurityAuthenticationSuccessHandler 处理认证成功后的响应
public class SecurityAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
	@Override
	public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
		if ("XMLHttpRequest".equalsIgnoreCase(request.getHeader("X-Requested-With"))) {
			// 判定为 Ajax 请求
			response.setContentType("application/json;charset=utf-8");
			PrintWriter writer = response.getWriter();
			writer.write("{\"succeed\":true}");
			writer.flush();
			writer.close();
			return;
		}
		super.onAuthenticationSuccess(request, response, authentication);
	}
}
  1. 编写 SecurityAuthenticationFailureHandler 处理认证失败后的响应
public class SecurityAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
	@Override
	public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
		if ("XMLHttpRequest".equalsIgnoreCase(request.getHeader("X-Requested-With"))) {
			// 判定为 Ajax 请求
			response.setContentType("application/json;charset=utf-8");
			PrintWriter writer = response.getWriter();
			writer.write("{\"succeed\":false}");
			writer.flush();
			writer.close();
			return;
		}
		super.onAuthenticationFailure(request, response, exception);
	}
}
  1. 修改 Security 配置类,指定认证成功和认证失败的处理器
@Configuration
public class SecurityConfigure extends WebSecurityConfigurerAdapter {

	@Resource
	private UserService service;

	@Bean
	public PasswordEncoder passwordEncoder() {
		return new BCryptPasswordEncoder();
	}

	@Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
		auth.userDetailsService(service);
	}

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http.logout()
				// 自定义注销页面的路由
				.logoutUrl("/security/logout").permitAll();

		http.formLogin()
				// 自定义登录页面的路由
				.loginPage("/security/login")
				// 自定义登录处理地址,Security 会自定处理,不需要编写对应的控制器
				// 该地址可以不用指定,默认和 loginPage 是一致的
				.loginProcessingUrl("/security/login-processor")
				// 指定认证成功后的处理器
				.successHandler(new SecurityAuthenticationSuccessHandler())
				// 指定认证失败后的处理器
				.failureHandler(new SecurityAuthenticationFailureHandler())
				.permitAll();

		http.authorizeRequests()
				// 特定地址不需要认证,直接允许被访问
				.antMatchers("/favicon.ico", "/assets/**").permitAll();

		http.authorizeRequests()
				// 任何请求,都需要认证
				.anyRequest().authenticated();

	}
}
  1. 修改前端的请求
<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org">

<head>
    <meta charset="utf-8">
    <title>用户登录</title>
    <link th:href="@{/assets/css/login.css}" rel="stylesheet">
</head>

<body>
<div class="login-container">
    <h3>用户登录</h3>
    <form method="post" th:action="@{/security/login-processor }" autocomplete="off">
        <div class="form-item">
            <input type="text" name="username" placeholder="请输入登录账号"/>
        </div>
        <div class="form-item">
            <input type="password" name="password" placeholder="请输入登录密码"/>
        </div>
        <div class="form-item">
            <button type="button" onclick="handleSubmit()">登录</button>
        </div>
    </form>
    <script th:src="@{/assets/js/jquery.min.js}"></script>
    <script>
        function handleSubmit() {
            let f = document.forms[0];
            console.log(f.action);
            $.ajax({
                method: 'POST',
                url: f.action,
                headers: {"X-Requested-With": "XMLHttpRequest"},
                data: {
                    // security 默认开启 CSRF, 故而需要在提交信息的时候带上该参数
                    _csrf: f._csrf.value,
                    username: f.username.value,
                    password: f.password.value
                }
            }).then(function (response) {
                if (response && response.succeed) {
                    location.href = "/";
                } else {
                    alert("账号或密码错误");
                    f.reset();
                }
            });
        }
    </script>
    <div class="login-footer"> springboot security test </div>
</div>
</body>
</html>