Spring Security的安全管理两个重要概念是:认证(Authentication)和授权(Authorization)。认证即对用户登录进行管控,授权即对登录用户的权限进行管控。
添加spring-boot-starter-security启动器,pom中引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
一旦项目中引入spring-boot-starter-security启动器,安全功能会立即生效,启动项目后控制台会打印Using generated security password:...
,这个即为默认密码,默认用户名为“user”。登录首页会跳转到自带的登录页面,输入该账号密码即可进入。
通过自定义WebSecurityConfigurerAdapter类型的Bean组件重写void configure(AuthenticationManagerBuilder auth)方法,实现自定义用户认证。
Spring Security提供了多种自定一认证方式:
最简单的认证方式,一般用于测试,需要重写configure(AuthenticationManagerBuilder auth)方法。
package com.xc.config;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
/**
* @author wyp
*/
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//设置密码编码器
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
//设置内存用户信息
auth.inMemoryAuthentication().passwordEncoder(encoder)
.withUser("zs").password(encoder.encode("123456")).roles("common")
.and()
.withUser("ls").password(encoder.encode("123456")).roles("vip");
}
}
- @EnableWebSecurity注解开启MVC Security安全支持,等同于@EnableGlobalAuthentication、@Configuration、@Import的组合使用。
- 从Spring Security 5开始,自定义用户认证必须设置密码编码器用于保护密码,否则会异常。
- Spring Security有多种编码器,BCryptPasswordEncoder、Pbkdf2PasswordEncoder、SCryptPasswordEncoder等。
- 自定义用户时可定义用户角色roles和权限authorities,也可以一次添加多个角色或权限,如
roles("common","vip")
和authorities("ROLE_common","ROLE_vip")
是等效的。
创建三个表customer_tb(用户表)、authority_tb(权限表)、customer_authority_tb(用户权限关联表)。
CREATE TABLE `authority_tb` (
`id` int(10) NOT NULL AUTO_INCREMENT,
`authority` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '权限',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
INSERT INTO `authority_tb` VALUES (1, 'ROLE_common');
INSERT INTO `authority_tb` VALUES (2, 'ROLE_vip');
SET FOREIGN_KEY_CHECKS = 1;
CREATE TABLE `customer_tb` (
`id` int(10) NOT NULL AUTO_INCREMENT,
`username` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`password` varchar(200) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`valid` tinyint(1) NOT NULL DEFAULT 1 COMMENT '校验用户是否合法,默认合法1',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
INSERT INTO `customer_tb` VALUES (1, 'zs', '$2a$10$X5/MLB1vMYOAF9./ib9aROrmeaoBLuvHxSw9XPoMLDJCgrjInofty', 1);
INSERT INTO `customer_tb` VALUES (2, 'ls', '$2a$10$X5/MLB1vMYOAF9./ib9aROrmeaoBLuvHxSw9XPoMLDJCgrjInofty', 1);
SET FOREIGN_KEY_CHECKS = 1;
CREATE TABLE `customer_authority_tb` (
`id` int(10) NOT NULL AUTO_INCREMENT,
`customer_id` int(10) NULL DEFAULT NULL COMMENT '用户id',
`authority_id` int(10) NULL DEFAULT NULL COMMENT '权限id',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
INSERT INTO `customer_authority_tb` VALUES (1, 1, 1);
INSERT INTO `customer_authority_tb` VALUES (2, 2, 2);
SET FOREIGN_KEY_CHECKS = 1;
注意: 由于Security进行用户查询时是先通过username定位用户是否唯一的,所以customer_tb的username字段必须唯一;customer_tb必须定义一个tinyint类型的字段(对应boolean类型的属性,如上面创建的valid),用于校验用户身份是否合法;插入的密码必须对应编码器编码后的密码;customer_tb的权限值必须带有“ROLE_”前缀,而默认的用户角色值则是对应权限去掉前缀“ROLE_”。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
此处省略全局配置…
package com.xc.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import javax.sql.DataSource;
/**
* 注解:@EnableWebSecurity-开启MVC Security安全支持
* @author wyp
*/
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//设置密码编码器
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
//使用JDBC进行身份验证
String userSql = "select username,password,valid from customer_tb where username = ?";
String authoritySql = "select c.username,a.authority " +
"from customer_tb c,authority_tb a,customer_authority_tb ca " +
"where ca.customer_id=c.id and ca.authority_id=a.id and c.username=?";
auth.jdbcAuthentication().passwordEncoder(encoder)
.dataSource(dataSource)
.usersByUsernameQuery(userSql)
.authoritiesByUsernameQuery(authoritySql);
}
}
- 定义用户查询的sql语句时,必须返回用户名username、密码password、是否为有效用户valid三个字段信息;
- 定义权限查询的sql语句时,必须返回用户名username、权限authority两个字段信息;
- usersByUsernameQuery(String query)设置用于通过用户名查找用户的查询;
- authoritiesByUsernameQuery(String query)设置用于通过用户名查找用户权限的查询。
频繁的使用jdbc进行数据查询认证麻烦且会降低网站的响应速度,推荐使用UserDetailsService身份认证。
如某些业务已经实现了用户信息的查询服务,就没必要再使用JDBC进行身份认证了。
CustomerServiceImpl.java
package com.xc.service;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.xc.entity.Authority;
import com.xc.entity.Customer;
import com.xc.entity.CustomerAuthority;
import com.xc.mapper.AuthorityMapper;
import com.xc.mapper.CustomerAuthorityMapper;
import com.xc.mapper.CustomerMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;
/**
* @author wyp
*/
@Service
public class CustomerServiceImpl implements CustomerService{
@Autowired
private CustomerMapper customerMapper;
@Autowired
private AuthorityMapper authorityMapper;
@Autowired
private CustomerAuthorityMapper customerAuthorityMapper;
@Resource
private RedisTemplate<String,Object> redisTemplate;
@Override
public Customer findCustomerByName(String username) {
Customer customer;
Object o = redisTemplate.opsForValue().get("customer::" + username);
if (o != null) {
customer = (Customer) o;
}else {
QueryWrapper<Customer> customerQueryWrapper = new QueryWrapper<>();
customerQueryWrapper.eq("username",username);
customer = customerMapper.selectOne(customerQueryWrapper);
if (customer != null) {
redisTemplate.opsForValue().set("customer::" + username,customer);
}
}
return customer;
}
@Override
public List<Authority> findAuthorityByName(String username) {
List<Authority> authorityList;
Object o = redisTemplate.opsForValue().get("authorityList::" + username);
if (o != null) {
authorityList = (List<Authority>) o;
}else {
authorityList = ...; //此处省略查询步骤
if (authorityList.size()>0) {
redisTemplate.opsForValue().set("authorityList::" + username,authorityList);
}
}
return authorityList;
}
}
UserDetailsService是Security提供的用于封装认证用户信息的接口,通过重写loadUserByUsername(String username)方法通过用户名加载用户信息。
package com.xc.config.service;
import com.xc.entity.Authority;
import com.xc.entity.Customer;
import com.xc.service.CustomerService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
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 java.util.List;
import java.util.stream.Collectors;
/**
* @author wyp
*/
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private CustomerService customerService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//获取用户和权限信息
Customer customer = customerService.findCustomerByName(username);
//如果用户不存在,必须抛出异常,否则会导致整体报错
if (customer == null) {
throw new UsernameNotFoundException("当前用户不存在");
}
List<Authority> authorityList = customerService.findAuthorityByName(username);
//对用户权限进行封装
List<SimpleGrantedAuthority> list = authorityList.stream()
.map(authority -> new SimpleGrantedAuthority(authority.getAuthority())).collect(Collectors.toList());
//返回封装的UserDetails用户详情类
return new User(customer.getUsername(), customer.getPassword(), list);
}
}
package com.xc.config;
import com.xc.config.service.UserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
/**
* 注解:@EnableWebSecurity-开启MVC Security安全支持
* @author wyp
*/
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsServiceImpl userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//设置密码编码器
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
//使用userDetailsService进行身份认证
auth.userDetailsService(userDetailsService).passwordEncoder(encoder);
}
}
HttpSecurity类的主要方法及说明
方法 | 说明 |
---|---|
authorizeRequests() | 开启基于HttpServletRequest请求访问的限制 |
formLogin() | 开启基于表单的用户登录 |
httpBasic() | 开启基于HTTP请求的Basic认证登录 |
logout() | 开启退出登录的支持 |
sessionManagement() | 开启Session管理配置 |
rememberMe() | 开启记住我功能 |
csrf() | 配置CSRF跨站请求伪造防护功能 |
通过authorizeRequests()开启
用户访问控制主要方法及说明
方法 | 描述 |
---|---|
antMatchers(String… antPatterns) | 开启Ant风格的路径匹配 |
mvcMatchers(String… mvcPatterns) | 开启MVC风格的路径匹配 |
regexMatchers(String… regexPatterns) | 开启正则表达式的路径匹配 |
and() | 功能连接符 |
anyRequest() | 匹配任何请求 |
rememberMe() | 开启记住我功能 |
access(String attribute) | 匹配给定的SpEL表达式计算结果是否为true |
hasAnyRole(String… authorities) | 匹配用户是否有参数中的任意角色 |
hasRole(String role) | 匹配用户是否有某一个角色 |
hasAnyAuthority(String… authorities) | 匹配用户是否有参数中的任意权限 |
hasAuthority(String authority) | 匹配用户是否有某一权限 |
authenticated() | 匹配已经登录认证的用户 |
fullyAuthenticated() | 匹配完整登录认证的用户,而非记住我登录用户 |
hasIpAddress(String ipaddressExpression) | 匹配某IP地址的请求访问 |
permitAll() | 无条件对请求进行放行 |
示例:
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/").permitAll()
.antMatchers("/detail/common/**").hasRole("common")
.antMatchers("/detail/vip/**").hasRole("vip")
//其他请求要求用户必须进行登录认证
.anyRequest().authenticated()
.and()
.formLogin();
}
}
之前一直使用的自带的登录界面,但通常登录界面要自己定制。
通过formLogin()开启。
用户登录主要方法及说明
方法 | 描述 |
---|---|
loginPage(String loginPage) | 用户登录页面跳转路径,默认为get请求的/login |
successForwardUrl(String forwardUrl) | 用户登录成功后的重定向地址 |
successHandler(AuthenticationSuccessHandler successHandler) | 用户登录成功后的处理 |
defaultSuccessUrl(String defaultSuccessUrl) | 用户直接登录后默认跳转地址 |
failureForwardUrl(String forwardUrl) | 用户登录失败后的重定向地址 |
failureUrl(String authenticationFailureUrl) | 用户登录失败后的跳转地址,默认/login?error |
failureHandler(AuthenticationFailureHandler authenticationFailureHandler) | 用户登录失败后的处理结果 |
usernameParameter(String usernameParameter) | 登录用户的用户名参数,默认username |
passwordParameter(String passwordParameter) | 登录用户的密码参数,默认password |
loginProcessingUrl(String loginProcessingUrl) | 登录表单提交的路径,默认为post请求的/login |
permitAll() | 无条件对请求进行放行 |
resources目录的templates下新建login目录,创建login.html,静态资源(图片css等)放在static/login下
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>登录</title>
<link rel="stylesheet" th:href="@{/login/css/login.css}">
</head>
<body th:style="'background-image: url(/login/img/back.jpg)'">
<div>
<form th:action="@{/userLogin}" method="post" style="width: 500px;margin: 200px auto;border: 2px gray solid;text-align: center">
<h2>登录</h2>
<!--错误信息登录提示框-->
<div th:if="${param.error}" style="color: red">用户名或密码错误</div>
<label>
<input type="text" name="name" placeholder="用户名">
</label><br>
<label>
<input type="text" name="password" placeholder="密码">
</label><br>
<div>
<label>
<input type="checkbox" value="remember-me">
</label>记住我
</div><br>
<input type="submit" value="登录"><br>
</form>
</div>
</body>
</html>
其中,登录提交方式必须是post,th:if="${param.error}"判断请求中是否带有error参数,该参数的Security默认的,用户也可以自定义。
定义登录跳转控制层
@GetMapping("/userLogin")
public String toLoginPage() {
return "login/login";
}
然后定义用户登录控制,重写configure(HttpSecurity http)方法:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
//放行静态资源
.antMatchers("/login/**").permitAll()
.antMatchers("/detail/common/**").hasRole("common")
.antMatchers("/detail/vip/**").hasRole("vip")
.anyRequest().authenticated();
http.formLogin()
.loginPage("/userLogin").permitAll()
.loginProcessingUrl("/userLogin")
.usernameParameter("name").passwordParameter("password")
.defaultSuccessUrl("/")
.failureUrl("/userLogin?error");
}
注意:
使用HttpSecurity类的logout()方法处理用户退出,会清除Session和记住我等任何默认用户配置。
用户退出主要方法及说明
方法 | 说明 |
---|---|
logoutUrl(String logoutUrl) | 用户退出处理控制URL,默认post请求的/logout |
logoutSuccessUrl(String logoutSuccessUrl) | 用户退出成功后的重定向地址 |
logoutSuccessHandler(LogoutSuccessHandler logoutSuccessHandler) | 用户退出成功后的处理器设置 |
deleteCookies(String… cookieNamesToClear) | 用户退出后删除指定的Cookie |
invalidateHttpSession(boolean invalidateHttpSession) | 用户退出后是否立即清除Session,默认true |
clearAuthentication(boolean clearAuthentication) | 用户退出后是否立即清除Authentication用户认证信息,默认true |
首页添加注销按钮:
<form th:action="@{/myLogout}" method="post">
<input type="submit" value="注销">
</form>
在configure(HttpSecurity http)方法中新增退出:
http.logout()
.logoutUrl("/myLogout")
.logoutSuccessUrl("/");
获取用户信息通常用以下两种方式:HttpSession和SecurityContextHolder。(需要关闭csrf防护http.csrf().disable();
)
使用HttpSession获取用户信息:
@GetMapping("/getUserBySession")
public void getUser(HttpSession session) {
// 从当前HttpSession获取绑定到此会话的所有对象的名称
Enumeration<String> names = session.getAttributeNames();
while (names.hasMoreElements()) {
// 获取HttpSession中会话名称
String element = names.nextElement();
// 获取HttpSession中的应用上下文
SecurityContextImpl attribute = (SecurityContextImpl) session.getAttribute(element);
System.out.println("element: " + element);
System.out.println("attribute: " + attribute);
// 获取用户相关信息
Authentication authentication = attribute.getAuthentication();
UserDetails principal = (UserDetails) authentication.getPrincipal();
System.out.println(principal);
System.out.println("username: " + principal.getUsername());
}
}
会话中有一个key为“SPRING_SECURITY_CONTEXT”的用户信息(即element输出的信息),UserDetails中封装了用户的主要信息,如子用户名、权限等。
使用SecurityContextHolder获取用户信息:
@GetMapping("/getUserByContext")
public void getUserByContext() {
//获取应用上下文
SecurityContext context = SecurityContextHolder.getContext();
System.out.println("context = " + context);
//获取用户相关信息
Authentication authentication = context.getAuthentication();
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
System.out.println("userDetails = " + userDetails);
System.out.println("username = " + userDetails.getUsername());
}