Spring Security处理登录的流程
Spring Security 的登录流程描述:
-
用户提交登录表单,包含用户名和密码。
-
UsernamePasswordAuthenticationFilter 过滤器拦截请求,并将用户名和密码封装成
UsernamePasswordAuthenticationToken 对象。 -
AuthenticationManager 调用配置的 AuthenticationProvider 列表中的每个提供者进行身份验证。
-
AuthenticationProvider 检查用户名和密码是否匹配,如果匹配成功,则返回一个已验证的 Authentication
对象。 -
如果所有的 AuthenticationProvider 都未能成功验证身份,则认证失败并抛出异常。
-
认证成功后,AuthenticationSuccessHandler 处理器处理认证成功的操作,可以将必要的信息存储在 session
中。 -
如果没有自定义的认证成功处理器,将使用默认的 SavedRequestAwareAuthenticationSuccessHandler
处理器重定向用户到最初尝试访问的页面。 -
在用户注销时,LogoutFilter 捕获注销请求,调用 LogoutHandler 来清除用户的凭证。
Spring Security 常见的组件:
-
认证管理器(Authentication Manager):处理用户身份验证请求,核心组件之一。
-
用户详情服务(UserDetailsService):用于从数据源加载用户信息并构建 UserDetails
对象,供认证管理器验证用户身份。 -
权限鉴定处理器(AccessDecisionManager):决定是否允许用户访问特定资源,例如 URL、页面或方法。
-
过滤器链(Filter Chain):由多个过滤器组成的链,用于在请求到达应用程序之前拦截并处理请求。
-
安全上下文(Security Context):存储有关当前用户的安全信息,例如 Authentication 和
GrantedAuthority 对象。 -
表达式语言(Expression Language):用于定义一些复杂的安全规则,例如基于角色或权限的访问控制。
-
CSRF 保护(CSRF Protection):防止跨站请求伪造攻击。
-
Remember-Me 认证(Remember-Me Authentication):允许用户在会话失效或注销后仍然保持登录状态。
-
Web 安全性(Web Security):定义对特定 URL 的安全性规则和配置。
-
OAuth 2.0 认证(OAuth 2.0 Authentication):用于支持 OAuth 2.0 流程的组件。
处理流程图
- 前段传入参数username、password(AdminLoginInfoDTO)到AdminController。
@RestController
@RequestMapping("/admins")
public class AdminController {
@Autowired
private IAdminService adminService;
@PostMapping("/login")
public JsonResult<Void> login(AdminLoginInfoDTO adminLoginInfoDTO) {
return JsonResult.ok();
}
- 在AdminController调用IAdminService(AdminServiceImpl为实现类)接口的login()方法并将AdminLoginInfoDTO作为参数传入。
@RestController
@RequestMapping("/admins")
public class AdminController {
@Autowired
private IAdminService adminService;
@PostMapping("/login")
public JsonResult<Void> login(AdminLoginInfoDTO adminLoginInfoDTO) {
adminService.login(adminLoginInfoDTO);
return JsonResult.ok();
}
- 通过 new UsernamePasswordAuthenticationToken传入参数username、password返回一个份验证令牌authenticationToken 。
@Service
public class AdminServiceImpl implements IAdminService {
@Autowired
private AdminMapper adminMapper;
@Autowired
private AuthenticationManager authenticationManager
@Override
public void login(AdminLoginInfoDTO adminLoginInfoDTO) {
Authentication authentication = new UsernamePasswordAuthenticationToken(
adminLoginInfoDTO.getUsername(), adminLoginInfoDTO.getPassword());
}
- 通过身份验证管理器(authenticationManager)的 authenticate() 方法执行身份验证操作,用于返回一个已验证的 Authentication 对象。
@Service
public class AdminServiceImpl implements IAdminService {
@Autowired
private AdminMapper adminMapper;
@Autowired
private AuthenticationManager authenticationManager
@Override
public void login(AdminLoginInfoDTO adminLoginInfoDTO) {
Authentication authentication = new UsernamePasswordAuthenticationToken(
adminLoginInfoDTO.getUsername(), adminLoginInfoDTO.getPassword());
Authentication authenticateResult
= authenticationManager.authenticate(authentication);
}
- 使用身份验证管理器(authenticationManager)调用 authenticate() 时会自动触发loadUserByUsername(自定义类UserDetailsServiceImpl的,实现接口UserDetailsService重写的方法)方法的调用,因为在进行身份验证时,需要根据提供的用户名(username)从数据源中获取用户的详细信息。
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private AdminMapper adminMapper;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
}
- 自动装配AdminMapper,调用getLoginInfoByUsername。
- 查询数据库。
- 获得查询结果。
- 将查询结果loginInfo返回给类UserDetailsServiceImpl。
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private AdminMapper adminMapper;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
AdminLoginInfoVO loginInfo = adminMapper.getLoginInfoByUsername(s);
}
- 取出loginInfo中的权限信息授予给用户。自定义AdminDetails类扩展默认的(UserDetails)用户详细信息,通过new AdminDetails将loginInfo的属性作为参数传入,并传入类AdminDetails扩展的值(如id),返回得到adminDetails对象。再将该对象作为返回值返会给authenticationManager完成第四步的验证。
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private AdminMapper adminMapper;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
AdminLoginInfoVO loginInfo = adminMapper.getLoginInfoByUsername(s);
if (loginInfo == null) {
return null;
}
/*GrantedAuthority是Spring Security框架中表示授予给用户的权限的接口。
通常情况下,该接口由具体的类来实现,该类包含了相应的权限信息。在实际使用中,
需要根据业务需求自定义实现特定权限的类,并将其添加到用户的权限列表中*/
List<GrantedAuthority> authorities = new ArrayList<>();
List<String> permissions = loginInfo.getPermissions();
for (String permission : permissions) {
/* SimpleGrantedAuthority是Spring Security框架中一个简单的实现了GrantedAuthority
接口的类。它表示授予给用户的权限,并提供了一些方法来方便地创建和管理权限。*/
authorities.add(new SimpleGrantedAuthority(permission));
}
/*UserDetails: java自带的接口,但是为了扩展默认的用户详细信息,选择了自定义
AdminDetails类继承User(User中实现了UserDetails接口),便可在该类扩展默
认的用户详细信息*/
AdminDetails adminDetails = new AdminDetails(
loginInfo.getId(),
loginInfo.getUsername(),
loginInfo.getPassword(),
loginInfo.getEnable() == 1,
authorities);
return adminDetails;
- 若认证成功,返回一个Authentication对象,若认证失败抛出异常。
- 认证通过,将认证结果对象存入SecurityContext。认证失败返回给AdminController一个失败结果。
- 将认证结果响应给前端。
组件详解:
UsernamePasswordAuthenticationToken:
传入用户名密码生成包含参数数据的验证令牌Token.里面包含以下数据。
- Principal(主体):代表认证的主体,通常是用户的用户名或其他标识符。可以通过getPrincipal()方法获取该值。
- Credentials(凭证):表示主体的密码、密钥或其他凭证信息。可以通过getCredentials()方法获取该值。
- Authorities(权限):表示已经被验证的主体所拥有的权限集合。通过getAuthorities()方法获取一个包含权限的集合对象。
- Details(细节):包含有关认证请求的其他详细信息,例如IP地址、请求时间等。可以通过getDetails()方法获取该值。
authenticationManager:
它负责处理从应用程序中的任何位置提出的身份验证请求,并决定该请求是否通过。传入验证令牌返回包含用户身份信息的完整身份验证对象。该对象包含以下内容:
-
Principal(主体):代表认证的主体,通常是用户的用户名或其他标识符。可以通过getPrincipal()方法获取该值。
-
Credentials(凭证):表示主体的密码、密钥或其他凭证信息。可以通过getCredentials()方法获取该值。
-
Authorities(权限):表示已经被验证的主体所拥有的权限集合。通过getAuthorities()方法获取一个包含权限的集合对象。
-
Details(细节):包含有关认证请求的其他详细信息,例如IP地址、请求时间等。可以通过getDetails()方法获取该值。
-
认证状态信息 :表示该身份验证对象是否通过验证。可以通过isAuthenticated()方法获取该值。
AuthenticationException
认证失败即抛出AuthenticationException或其子类的异常,以表示身份验证失败。
常见的AuthenticationException及其子类异常包括:
- BadCredentialsException:表示凭证不正确(例如密码错误)。
- LockedException:表示用户已被锁定,无法登录。
- DisabledException:表示用户已被禁用,无法登录。
- AccountExpiredException:表示用户的帐户已过期,无法登录。
- CredentialsExpiredException:表示用户的凭证(例如密码)已过期,需要进行更新。
SecurityContext:
认证成功将认证结果对像存入SecurityContext
SecurityContext包含以下信息:
- 认证信息 :表示当前已经通过身份验证的用户信息,包括用户名、密码、权限等等。通常是一个实现了Spring
Security中的Authentication接口的Java对象。可以通过SecurityContext.getAuthentication()方法获取该值。 - 上下文配置信息 :表示与当前上下文相关的其他安全配置信息,例如安全策略、登录页面、超时时间等等。通常是一个实现了Spring
Security中的SecurityContextConfiguration接口的Java对象。可以通过SecurityContextHolder.getContext().getAuthentication().getDetails()方法获取该值。
UserDetailsServiceImpl:
Spring Security框架中一个实现了UserDetailsService接口的服务类,用于查询用户信息并提供用户身份验证。
UserDetailsService接口包含一个仅有的方法loadUserByUsername(),该方法用于根据用户名从存储库中查找用户信息并返回用户详细信息对象。该用户详细信息对象需要实现Spring Security中的UserDetails接口,并包含以下信息:
-
用户名
-
密码
-
是否启用
-
是否锁定
-
是否过期
-
是否认证过期
-
用户权限列表
当有用户凭证被传递给authenticationManager进行身份验证时,loadUserByUsername()方法会被自动调用,并返回与该用户名对应的UserDetails对象,以进行后续身份验证和授权操作
UserDetails接口:是表示用户详细信息的核心接口。它定义了一些用于访问用户的用户名、密码、权限等信息的方法。
AdminDetails为什么需要自定义继承User:
java自带的接口,但是为了扩展默认的用户详细信息,选择了自定义
AdminDetails类继承User(User中实现了UserDetails接口),便可在该类扩展默认的用户详细信息。
具体原因如下:
- 存储额外信息: 默认的UserDetails接口并不包含与管理员相关的额外信息,例如管理员邮箱、创建时间等。通过自定义AdminDetails类继承UserDetails接口,可以在该类中添加额外的属性来存储管理员的附加信息。
- 扩展授权信息: 默认的UserDetails接口中包含用户的权限信息,通常使用GrantedAuthority对象的列表表示。通过自定义AdminDetails类,可以扩展这个权限列表,以包含管理员特定的权限信息。
- 提供定制化方法: 自定义AdminDetails类可以添加其他方法,以便在应用程序中使用管理员详细信息时提供更多的定制化功能。
- 与数据源交互: 自定义AdminDetails类可能需要与数据库或其他数据源进行交互,以获取管理员的详细信息。通过继承UserDetails接口,可以在自定义的AdminDetailsService中实现与数据源的交互,并将获取到的信息填充到AdminDetails对象中。
总而言之,通过自定义AdminDetails类继承UserDetails接口,可以实现对管理员详细信息的自定义、扩展和集成,以满足特定应用程序的需求。这样,可以更好地管理和使用管理员的身份验证和相关信息,并使其与Spring Security框架无缝集成。
完整代码
AdminController
@RestController
@RequestMapping("/admins")
public class AdminController {
@Autowired
private IAdminService adminService;
@PostMapping("/login")
public JsonResult<Void> login(AdminLoginInfoDTO adminLoginInfoDTO) {
log.debug("开始处理【管理员登录】的请求,参数:{}", adminLoginInfoDTO);
adminService.login(adminLoginInfoDTO);
return JsonResult.ok();
}
IAdminService
@Transactional
public interface IAdminService {
/**
* 管理员登录
* @param adminLoginInfoDTO 封装了用户名、密码等相关信息的对象
*/
void login(AdminLoginInfoDTO adminLoginInfoDTO);
AdminServiceImpl
@Service
public class AdminServiceImpl implements IAdminService {
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private AdminMapper adminMapper;
@Autowired
private AuthenticationManager authenticationManager
@Override
public void login(AdminLoginInfoDTO adminLoginInfoDTO) {
Authentication authentication = new UsernamePasswordAuthenticationToken(
adminLoginInfoDTO.getUsername(), adminLoginInfoDTO.getPassword());
Authentication authenticateResult
= authenticationManager.authenticate(authentication);
SecurityContext securityContext = SecurityContextHolder.getContext();
securityContext.setAuthentication(authenticateResult);
}
UserDetailsServiceImpl
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private AdminMapper adminMapper;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
AdminLoginInfoVO loginInfo = adminMapper.getLoginInfoByUsername(s);
if (loginInfo == null) {
return null;
}
/*GrantedAuthority是Spring Security框架中表示授予给用户的权限的接口。
通常情况下,该接口由具体的类来实现,该类包含了相应的权限信息。在实际使用中,
需要根据业务需求自定义实现特定权限的类,并将其添加到用户的权限列表中*/
List<GrantedAuthority> authorities = new ArrayList<>();
List<String> permissions = loginInfo.getPermissions();
for (String permission : permissions) {
/* SimpleGrantedAuthority是Spring Security框架中一个简单的实现了GrantedAuthority
接口的类。它表示授予给用户的权限,并提供了一些方法来方便地创建和管理权限。*/
authorities.add(new SimpleGrantedAuthority(permission));
}
/*UserDetails: java自带的接口,但是为了扩展默认的用户详细信息,选择了自定义
AdminDetails类继承User(User中实现了UserDetails接口),便可在该类扩展默
认的用户详细信息*/
AdminDetails adminDetails = new AdminDetails(
loginInfo.getId(),
loginInfo.getUsername(),
loginInfo.getPassword(),
loginInfo.getEnable() == 1,
authorities);
return adminDetails;
}
}
AdminDetails
@Getter
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
public class AdminDetails extends User {
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;
}
}
AdminLoginInfoDTO
@Data
public class AdminLoginInfoDTO implements Serializable {
/**
* 用户名
*/
private String username;
/**
* 密码(原文)
*/
private String password;
}
AdminLoginInfoVO
@Data
public class AdminLoginInfoVO implements Serializable {
/**
* 数据id
*/
private Long id;
/**
* 用户名
*/
private String username;
/**
* 密码(密文)
*/
private String password;
/**
* 是否启用,1=启用,0=未启用
*/
private Integer enable;
/**
* 权限列表
*/
private List<String> permissions;
}
AdminMapper
@Repository
public interface AdminMapper {
/**
* 根据用户名查询管理员的登录信息
* @param username 用户名
* @return 匹配的管理员的登录信息,如果没有匹配的数据,则返回null
*/
AdminLoginInfoVO getLoginInfoByUsername(String username);
SecurityConfiguration
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder() {
// return NoOpPasswordEncoder.getInstance();
return new BCryptPasswordEncoder();
}
// 【注意】配置AuthenticationManager对象时
// 不要使用authenticationManager()方法,如果使用此方法,在测试时可能导致死循环,从而内存溢出
// 必须使用authenticationManagerBean()方法
// @Bean
// @Override
// protected AuthenticationManager authenticationManager() throws Exception {
// return super.authenticationManager();
// }
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 处理未通过认证时导致的拒绝访问
http.exceptionHandling().authenticationEntryPoint(new AuthenticationEntryPoint() {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
response.setContentType("application/json; charset=utf-8");
PrintWriter writer = response.getWriter();
writer.println("{\n" +
" \"state\": 40100,\n" +
" \"message\": \"您当前未登录,请登录!\"\n" +
"}");
writer.close();
}
});
// 禁用“防止伪造的跨域攻击”的防御机制
http.csrf().disable();
// 白名单
// 使用1个星号,表示通配此层级的任意资源,例如:/admins/*,可以匹配:/admins/add-new、/admins/delete
// 但是,不可以匹配多个层级,例如:/admins/*,不可以匹配:/admins/9527/delete
// 使用2个连续的星号,表示通配若干层级的任意资源,例如:/admins/*,可以匹配:/admins/add-new、/admins/9527/delete
String[] urls = {
"/doc.html",
"/**/*.css",
"/**/*.js",
"/swagger-resources",
"/v2/api-docs",
"/admins/login" // 管理员登录的URL
};
// 基于请求的访问控制
http.authorizeRequests() // 对请求进行授权
.mvcMatchers(urls) // 匹配某些路径
.permitAll() // 直接许可,即不需要认证即可访问
.anyRequest() // 任意请求
.authenticated(); // 要求通过认证的
// 如果调用以下方法,当需要访问通过认证的资源,但是未通过认证时,将自动跳转到登录页面
// 如果未调用以下方法,将响应403
// http.formLogin();
// super.configure(http); // 不要保留调用父类同名方法的代码,不要保留!不要保留!不要保留!
}
}
AdminMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.tedu.csmall.passport.mapper.AdminMapper">
<!-- AdminLoginInfoVO getLoginInfoByUsername(String username); -->
<select id="getLoginInfoByUsername" resultMap="LoginInfoResultMap">
SELECT
<include refid="LoginInfoQueryFields"/>
FROM
ams_admin
LEFT JOIN ams_admin_role ON ams_admin.id = ams_admin_role.admin_id
LEFT JOIN ams_role_permission ON ams_admin_role.role_id = ams_role_permission.role_id
LEFT JOIN ams_permission ON ams_role_permission.permission_id = ams_permission.id
WHERE
username=#{username}
</select>
<!-- collection标签:用于配置1对多的查询,也可理解为配置List属性对应的值如何封装 -->
<!-- collection标签的property属性:与id或result标签的property属性相同 -->
<!-- collection标签的ofType属性:List中的元素类型 -->
<!-- collection标签的子级:如何创建List中的元素对象 -->
<resultMap id="LoginInfoResultMap" type="cn.tedu.csmall.passport.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>
<sql id="LoginInfoQueryFields">
<if test="true">
ams_admin.id,
ams_admin.username,
ams_admin.password,
ams_admin.enable,
ams_permission.value
</if>
</sql>
</mapper>