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.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-thymeleafartifactId>
dependency>
dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-dependenciesartifactId>
<version>2.1.16.RELEASEversion>
<type>pomtype>
<scope>importscope>
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
接口来完成。
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');
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>5.1.47version>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
<version>2.2.0version>
dependency>
spring:
datasource:
url: jdbc:mysql://localhost:3306/test?useSSL=false&serverTimeZone=GTM%2B8
driver-class-name: com.mysql.jdbc.Driver
username: root
password: root
@Mapper
public interface UserMapper {
/**
* 根据账号查询密码
* @param username 账号
*/
@Select("SELECT `password` FROM `security_user` WHERE `username` = #{0} LIMIT 1")
String selectPwdByUsername(String username);
}
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);
}
}
@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_user
中 username
字段和 password
字段的值来决定的。
@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();
}
}
@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:/";
}
}
需要注意的是,账号输入框的
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 登录页和注销页的自定义啦。
Security 在认证完成后,成功会默认执行 successHandler 中指定的
onAuthenticationSuccess
方法,失败会默认执行 failureHandler 中指定的onAuthenticationFailure
方法,可通过重写该方法实现对异步认证的处理
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);
}
}
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);
}
}
@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();
}
}
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>