Spring Security框架
添加Spring Security框架依赖
<!-- Spring Security依赖 主要解决认证与授权相关问题 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
Spring Security的配置类
package cn.tedu.csmallpassport.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Slf4j
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
}
package cn.tedu.csmallpassport.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Slf4j
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// super.configure(http); // 不要调用父类的同款方法
}
}
关于登录表单
package cn.tedu.csmallpassport.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Slf4j
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// super.configure(http); // 不要调用父类的同款方法
// 调用formLogin() 表示启用登录和登出页,如果未调用此方法,则没有登录和登出页
http.formLogin();
}
}
关于URL的授权访问
package cn.tedu.csmallpassport.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Slf4j
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
// 白名单
String[] urls = {
"/login"
};
@Override
protected void configure(HttpSecurity http) throws Exception {
// 配置URL的授权访问
// 注意: 配置时,各请求的授权访问遵循“第一匹配原则”,即根据代码从上至下,以第1次匹配到的规则为准
// 所以在配置时,必须将更加精准的配置写在前面,覆盖范围更大的匹配的配置写在后面
http.authorizeRequests() // 配置URL的授权访问
.mvcMatchers(urls) // 匹配某些请求
.permitAll() // 直接许可, 即 不需要认证就可以直接访问
.anyRequest() // 任何请求
.authenticated(); // 以上匹配到的请求必须是”已经通过认证的(通过登录的)“
// super.configure(http); // 不要调用父类的同款方法
// 调用formLogin() 表示启用登录和登出页,如果未调用此方法,则没有登录和登出页
http.formLogin();
}
}
使用临时的自定义账号实现登录
package cn.tedu.csmallpassport.security;
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;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
return null;
}
}
package cn.tedu.csmallpassport.security;
import lombok.extern.slf4j.Slf4j;
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;
@Slf4j
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
// 假设正确的用户名是root,匹配的密码是1234
if (!"root".equals(s)) {
log.warn("用户名【{}】错误,将不会返回有效的UserDetails(用户详情)");
return null;
}
UserDetails userDetails = User.builder() // 构建者
.username("root") // 存入用户名
.password("1234") // 存入密码
.disabled(false) // 存入启用或禁用的状态
.accountLocked(false) // 存入账户是否锁定的状态
.credentialsExpired(false) // 存入凭证是否过期的状态
.accountExpired(false) // 存入账户是否过期的状态
.authorities("这是一个临时的山寨权限,暂时没什么用") // 存入权限列表
.build(); // 执行构建,得到UserDetails类型的对象
log.debug("即将向Spring Security返回UserDetails类型的对象,返回结果:{}", userDetails);
return userDetails;
}
}
package cn.tedu.csmallpassport.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cglib.proxy.NoOp;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Slf4j
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
// 白名单
String[] urls = {
"/login"
};
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 配置URL的授权访问
// 注意: 配置时,各请求的授权访问遵循“第一匹配原则”,即根据代码从上至下,以第1次匹配到的规则为准
// 所以在配置时,必须将更加精准的配置写在前面,覆盖范围更大的匹配的配置写在后面
http.authorizeRequests() // 配置URL的授权访问
.mvcMatchers(urls) // 匹配某些请求
.permitAll() // 直接许可, 即 不需要认证就可以直接访问
.anyRequest() // 任何请求
.authenticated(); // 以上匹配到的请求必须是”已经通过认证的(通过登录的)“
// super.configure(http); // 不要调用父类的同款方法
// 调用formLogin() 表示启用登录和登出页,如果未调用此方法,则没有登录和登出页
http.formLogin();
}
}
使用数据库中的账号实现登录
package cn.tedu.csmallpassport.pojo.vo;
import lombok.Data;
import java.io.Serializable;
@Data
public class AdminLoginInfoVO implements Serializable {
private Long id;
private String username;
private String password;
private Integer enable;
}
AdminLoginInfoVO getLoginInfoByUsername(String username);
<select id="getLoginInfoByUsername" resultMap="LoginInfoResultMap">
SELECT id, username, password, enable FROM ams_admin WHERE username = #{username}
</select>
<resultMap id="LoginInfoResultMap" type="cn.tedu.csmallpassport.pojo.vo.AdminLoginInfoVO">
<id column="id" property="id"/>
<result column="username" property="username"/>
<result column="password" property="password"/>
<result column="enable" property="enable"/>
</resultMap>
@Test
void getLoginInfoByUsername() {
String username = "root";
AdminLoginInfoVO loginInfoByUsername = mapper.getLoginInfoByUsername(username);
System.out.println(loginInfoByUsername);
}
package cn.tedu.csmallpassport.security;
import cn.tedu.csmallpassport.mapper.AdminMapper;
import cn.tedu.csmallpassport.pojo.vo.AdminLoginInfoVO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
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;
@Slf4j
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
AdminMapper adminMapper;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
log.debug("Spring Security自动调用了loadUserByUsername()方法,参数:{}", s);
AdminLoginInfoVO loginInfo = adminMapper.getLoginInfoByUsername(s);
log.debug("根据用户名【{}】查询登录信息,结果:{}", s, loginInfo);
if (loginInfo == null) {
log.warn("用户名不存在,将无法返回有效的UserDetails对象,则返回null");
return null;
}
log.debug("开始创建返回给Spring Security的UserDetails对象");
UserDetails userDetails = User.builder() // 构建者
.username("root") // 存入用户名
.password("1234") // 存入密码
.disabled(loginInfo.getEnable() == 0) // 存入启用或禁用的状态
.accountLocked(false) // 存入账户是否锁定的状态
.credentialsExpired(false) // 存入凭证是否过期的状态
.accountExpired(false) // 存入账户是否过期的状态
.authorities("这是一个临时的山寨权限,暂时没什么用") // 存入权限列表
.build(); // 执行构建,得到UserDetails类型的对象
log.debug("即将向Spring Security返回UserDetails类型的对象,返回结果:{}", userDetails);
return userDetails;
}
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
http.csrf().disable();
关于防止伪造的跨域攻击
伪造的跨域攻击:此类攻击是基于“服务器对客户端的浏览器的信任”,例如,用户在浏览器的第1个选项卡中登录了,那么,在第2个、第3个等等同一个浏览器的其它选项卡中访问同样的服务器,也会被视为“已登录”的状态。所以,假设某个用户在浏览器的第1个选项卡中登录了网上银行,此用户在第2个选项卡中打开了另一个网站,此网站可能是恶意的网站(不是此前第1个选项卡的网上银行的网站),在恶意网站中隐藏了一个向网上银行的网站发起请求的链接,并自动发出了请求(比较典型的做法是将链接设置为标签的src属性值,并隐藏此标签使之不显示),则会导致在第2个选项卡中的恶意网站被打开时,就自动的向网上银行发起了请求,而网上银行收到了请求后,会视为“已登录”的状态!
以Spring Security默认的登录表单为例:
前后端分离的登录
// 白名单
// 所有路径必须使用 / 作为第1个字符
// 使用1个星号作为通配符时,表示通配此层级的任意资源,例如:/admins/*,可以匹配 /admins/delete、/admins/add-new
// 但是,不可以匹配多层级,例如:/admins/* 不可匹配 /admins/9527/delete
// 使用2个连续的星号,表示通配若干层级的任意资源,例如:/admins/** 可以匹配 /admins/delete、/admins/9527/delete
String[] urls = {
"/doc.html",
"/favicon.ico",
"/**/*.css",
"/**/*.js",
"/swagger-resources",
"/v2/api-docs",
};
@PostMapping("/login")
public JsonResult login(@RequestBody AdminLoginDTO adminLoginDTO) {
log.debug("开始处理【管理员登录】的请求,参数:{}", adminLoginDTO);
// TODO 具体功能待定
return JsonResult.ok();
}
// 白名单
// 所有路径必须使用 / 作为第1个字符
// 使用1个星号作为通配符时,表示通配此层级的任意资源,例如:/admins/*,可以匹配 /admins/delete、/admins/add-new
// 但是,不可以匹配多层级,例如:/admins/* 不可匹配 /admins/9527/delete
// 使用2个连续的星号,表示通配若干层级的任意资源,例如:/admins/** 可以匹配 /admins/delete、/admins/9527/delete
String[] urls = {
"/doc.html",
"/favicon.ico",
"/**/*.css",
"/**/*.js",
"/swagger-resources",
"/v2/api-docs",
"/admins/login" // 新增
};
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Autowired
private AuthenticationManager authenticationManager;
@Override
public void login(AdminLoginDTO adminLoginDTO) {
log.debug("开始处理【管理员登录】的业务,参数:{}", adminLoginDTO);
// 创建认证信息
Authentication authentication = new UsernamePasswordAuthenticationToken( // 第一个参数当事人, 第二个参数凭证
adminLoginDTO.getUsername(), adminLoginDTO.getPassword()
);
// 执行认证
authenticationManager.authenticate(authentication);
// 如果没有出现异常,则表示验证登录成功
log.debug("验证登录成功");
}
ERR_UNAUTHORIZED(401, "登录失败"),
ERR_UNAUTHORIZED_DISABLED(402, "账号被禁用"),
@ExceptionHandler({
InternalAuthenticationServiceException.class, BadCredentialsException.class
})
public JsonResult handleAuthenticationException(AuthenticationException e) {
log.debug("登录失败,用户名或密码错误!");
return JsonResult.fail(ServiceCode.ERR_UNAUTHORIZED, "登录失败,用户名或密码错误!");
}
@ExceptionHandler
public JsonResult handleDisabledException(DisabledException e) {
log.debug("登录失败,此账号已经被禁用!");
return JsonResult.fail(ServiceCode.ERR_UNAUTHORIZED_DISABLED, "登录失败,此账号已经被禁用!");
}
关于通过认证的标准
@Autowired
private AuthenticationManager authenticationManager;
@Override
public void login(AdminLoginDTO adminLoginDTO) {
log.debug("开始处理【管理员登录】的业务,参数:{}", adminLoginDTO);
// 创建认证信息
Authentication authentication = new UsernamePasswordAuthenticationToken( // 第一个参数当事人, 第二个参数凭证
adminLoginDTO.getUsername(), adminLoginDTO.getPassword()
);
Authentication authenticationResult = authenticationManager.authenticate(authentication);
// 如果没有出现异常,则表示验证登录成功,需要将认证信息存入到Security上下文中
log.debug("验证登录成功,即将向SecurityContext中存入Authentication");
SecurityContext securityContext = SecurityContextHolder.getContext();
securityContext.setAuthentication(authenticationResult);
}
关于authenticate()的返回结果
2023-04-04 14:45:03.800 DEBUG 6076 --- [nio-9081-exec-1] c.t.c.p.service.impl.AdminServiceImpl : 验证登录成功,返回的Authentication为:
UsernamePasswordAuthenticationToken [
Principal=org.springframework.security.core.userdetails.User [
Username=root,
Password=[PROTECTED],
Enabled=true,
AccountNonExpired=true,
credentialsNonExpired=true,
AccountNonLocked=true,
Granted Authorities=[这是一个临时的山寨权限,暂时没什么用]
],
Credentials=[PROTECTED],
Authenticated=true,
Details=null,
Granted Authorities=[这是一个临时的山寨权限,暂时没什么用]
]
识别当事人
package cn.tedu.csmallpassport.security;
import lombok.Getter;
import lombok.ToString;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import java.util.Collection;
@ToString(callSuper = true)
public class AdminDetails extends User {
@Getter
private Long id;
public AdminDetails(Long id, String username, String password, boolean enabled, Collection<? extends GrantedAuthority> authorities) {
super(username, password, enabled, true, true, true, authorities);
this.id = id;
}
}
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
log.debug("Spring Security自动调用了loadUserByUsername()方法,参数:{}", s);
AdminLoginInfoVO loginInfo = adminMapper.getLoginInfoByUsername(s);
log.debug("根据用户名【{}】查询登录信息,结果:{}", s, loginInfo);
if (loginInfo == null) {
String message = "用户名不存在,将无法返回有效的UserDetails对象,则返回null";
log.warn(message);
return null;
}
log.debug("开始创建返回给Spring Security的UserDetails对象……");
// ========== 以下是新的代码,替换了原有的代码 ==========
Collection<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority("这是一个临时的山寨权限,暂时没什么用"));
AdminDetails adminDetails = new AdminDetails(
loginInfo.getId(),
loginInfo.getUsername(),
loginInfo.getPassword(),
loginInfo.getEnable() == 1,
authorities
);
log.debug("即将向Spring Security返回UserDetails类型的对象,返回结果:{}", adminDetails);
return adminDetails;
}
处理授权
select ams_admin.id, ams_admin.username, ams_admin.password, ams_admin.enable, ams_permission.value
from ams_admin
left join ams_admin_role on ams_admin_role.admin_id = ams_admin.id
left join ams_role_permission on ams_role_permission.role_id = ams_admin_role.role_id
left join ams_permission on ams_permission.id = ams_role_permission.permission_id
where ams_admin.username = 'root'
<!-- AdminLoginInfoVO getLoginInfoByUsername(String username); -->
<select id="getLoginInfoByUsername" resultMap="LoginInfoResultMap">
SELECT
<include refid="LoginInfoQueryFields"/>
FROM ams_admin
LEFT JOIN ams_admin_role ON ams_admin_role.admin_id = ams_admin.id
LEFT JOIN ams_role_permission ON ams_role_permission.role_id = ams_admin_role.role_id
LEFT JOIN ams_permission ON ams_permission.id = ams_role_permission.permission_id
WHERE username = #{username}
</select>
<sql id="LoginInfoQueryFields">
<if test="true">
ams_admin.id,
ams_admin.username,
ams_admin.password,
ams_admin.enable,
ams_permission.value
</if>
</sql>
<!-- collection标签:配置List类型的属性 -->
<!-- collection标签的property属性:List类型的属性的名称 -->
<!-- collection标签的ofType属性:List属性的元素的数据类型,取值为元素类型的全限定名,java.lang包下的类可以省略包名 -->
<!-- collection标签的子级:配置如何创建出List集合中的各个元素对象 -->
<!-- collection标签的子级的constructor标签:通过构造方法创建对象 -->
<!-- constructor标签的子级的arg标签:配置构造方法的参数 -->
<resultMap id="LoginInfoResultMap" type="cn.tedu.csmallpassport.pojo.vo.AdminLoginInfoVO">
<id column="id" property="id"/>
<result column="username" property="username"/>
<result column="password" property="password"/>
<result column="enable" property="enable"/>
<collection property="permissions" ofType="java.lang.String">
<constructor>
<arg column="value"/>
</constructor>
</collection>
</resultMap>
2023-04-04 17:20:12.529 DEBUG 12956 --- [ main] c.t.c.passport.mapper.AdminMapperTests :
根据用户名【root】查询登录信息完成,查询结果:
AdminLoginInfoVO(
id=1,
username=root,
password=$2a$10$N.ZOn9G6/YLFixAOPMg/h.z7pCu6v2XyFDtC4q.jeeGm/TEZyj15C,
enable=1,
permissions=[
/ams/admin/read, /ams/admin/add-new,
/ams/admin/delete, /ams/admin/update,
/pms/product/read, /pms/product/add-new,
/pms/product/delete, /pms/product/update,
/pms/brand/read, /pms/brand/add-new,
/pms/brand/delete, /pms/brand/update,
/pms/category/read, /pms/category/add-new,
/pms/category/delete, /pms/category/update,
/pms/picture/read, /pms/picture/add-new,
/pms/picture/delete, /pms/picture/update,
/pms/album/read, /pms/album/add-new,
/pms/album/delete, /pms/album/update
]
)
package cn.tedu.csmallpassport.security;
import cn.tedu.csmallpassport.mapper.AdminMapper;
import cn.tedu.csmallpassport.pojo.vo.AdminLoginInfoVO;
import lombok.extern.slf4j.Slf4j;
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.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.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
@Slf4j
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
AdminMapper adminMapper;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
log.debug("Spring Security自动调用了loadUserByUsername()方法,参数:{}", s);
AdminLoginInfoVO loginInfo = adminMapper.getLoginInfoByUsername(s);
log.debug("根据用户名【{}】查询登录信息,结果:{}", s, loginInfo);
if (loginInfo == null) {
String message = "用户名不存在,将无法返回有效的UserDetails对象,则返回null";
log.warn(message);
return null;
}
log.debug("开始创建返回给Spring Security的UserDetails对象……");
// UserDetails userDetails = User.builder() // 构建者
// .username(loginInfo.getUsername()) // 存入用户名
// .password(loginInfo.getPassword()) // 存入密码
// .disabled(loginInfo.getEnable() == 0) // 存入启用或禁用的状态
// .accountLocked(false) // 存入账户是否锁定的状态
// .credentialsExpired(false) // 存入凭证是否过期的状态
// .accountExpired(false) // 存入账户是否过期的状态
// .authorities("这是一个临时的山寨权限,暂时没什么用") // 存入权限列表
// .build(); // 执行构建,得到UserDetails类型的对象
// log.debug("即将向Spring Security返回UserDetails类型的对象,返回结果:{}", userDetails);
// ========= 以下是本次调整代码 ===========
Collection<GrantedAuthority> authorities = new ArrayList<>();
for (String permission : loginInfo.getPermissions()) {
authorities.add(new SimpleGrantedAuthority(permission));
}
// =========== 以上是本次调整代码 ============
AdminDetails adminDetails = new AdminDetails(
loginInfo.getId(),
loginInfo.getUsername(),
loginInfo.getPassword(),
loginInfo.getEnable() == 1,
authorities
);
return adminDetails;
}
}
@Slf4j
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true) // 新增
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
// 暂不关心方法内部的代码
}
@ExceptionHandler
public JsonResult handleAccessDeniedException(AccessDeniedException e) {
log.debug("禁止访问,当前登录账号无权限访问!");
return JsonResult.fail(ServiceCode.ERR_FORBIDDEN, "禁止访问,当前登录账号无权限访问!");
}