文章目录
- 一、自定义数据源
- 1. 认证流程与原理分析
- AuthenticationManager、ProviderManager、AuthenticationProvider三者关系
- 2. 全局配置AuthenticationManager方式
- 由于WebSecurityConfigurerAdapter过期,我们使用以下写法:
- 3. 编码
- 3.1 创建数据库表与插入数据
- 3.2 创建实体类
- 3.3 导入Maven依赖与新增配置
- 3.4 编写UserMapper
- 3.5 编写MyUserDetailsService实现UserDetailsService接口
- 二、前后端分离案例—认证总结
- 1. 编写LoginFilter、修改SecurityFilterChain
- 2. 自定义数据源
- 三、前后端分离案例——添加验证码
- 1. 引入验证码依赖、编写验证码配置
- 2. 获取验证码接口
- 3. 验证码校验filter
- 4. 编写SpringSecurity配置类
一、自定义数据源
1. 认证流程与原理分析
https://docs.spring.io/spring-security/reference/servlet/authentication/architecture.html#servlet-authentication-abstractprocessingfilter
- 发起认证请求,携带用户名密码,请求被UsernamePasswordAuthenticationFilter拦截
- 在UsernamePasswordAuthenticationFilter的attemptAuthentication方法将请求的用户名和密码,封装为Authentication对象,交给AuthenticationManager进行认证
- 认证成功,将认证信息存储SecurityContextHolder以及时调用RememberMe等,并回调AuthenticationSuccessHandler处理
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication) throws AuthenticationException;
}
AuthenticationManager、ProviderManager、AuthenticationProvider三者关系
从分析可以知道,AuthenticationManager是认证类的核心类,但是实际上在底层实际认证的时候,是不能离开ProviderManager和AuthenticationProvider的。
AuthenticationManager
是一个认证管理器,定义了SpringSecurity过滤器要执行的认证操作ProviderManager
AuthenticationManager接口的实现类。SpringSecurity认证时默认使用的就是ProviderManager。AuthenticationProvider
就是针对不同身份类型执行的具体身份认证。
ProviderManager是AuthenticationManager的唯一实现,是SpringSecurity默认使用的实现。在默认情况下,AuthenticationManager 就是一个ProviderManager 。
在SpringSecurity中,允许系统同时支持多种不同的认证方式,eg:同时支持用户名/密码认证,RememberMe认证,手机号动态认证等,而不同的认证方式对应了不同的AuthenticationProvider,所以一个完整的认证流程,可能有多个AuthenticationProvider来提供。
多个AuthenticationProvider将组成一个list,这个列表由ProviderManager代理。即在ProviderManager中存在AuthenticationProvider列表,在ProviderManager中遍历列表中的每一个AuthenticationProvider去执行身份认证,最终得到认证结果。
ProviderManager本身也可以再配置一个AuthenticationManager作为parent,这样当ProviderManager认证失败之后,就可以进到parent再次认证。理论上,ProviderManager的parent可以是任意类型的AuthenticationManager,但是通常都是由ProviderManager来扮演parent的角色,也就是ProviderManager是ProviderManager的parent。
ProviderManager本身也可以有多个,多个ProviderManager共用一个parent。有时,一个应用程序有受保护资源的逻辑组(eg:所有符合路径的网络资源,/api/**),每个组可以有自己的专用AuthenticationManager。通常每个组都是一个ProviderManager,他们共用一个父级。然后,父级是一种全局资源,作为所有提供者的后备资源。
https://spring.io/guides/topicals/spring-security-architecture/
弄清楚认证原理后,我们来看具体认证时候数据源的获取。
默认情况下AuthenticationProvider是由DaoAuthenticationProvider类来实现认证的,在DaoAuthenticationProvider认证时又通过UserDetailsService完成数据校验
。关系如下图:
总结:AuthenticationManager是认证管理器,在SpringSecurity中有全局AuthenticationManager,也可以有局部AuthenticationManager。全局的AuthenticationManager用来对全局认证进行处理,局部的AuthenticationManager用来对某些特殊资源认证处理。当然无论是全局认证管理器还是局部认证管理器都是由ProviderManager来实现。每个ProviderManager中都代理一个AuthenticationProvider的列表,列表中每个实现代表一种身份认证方式。认证时底层数据源调用UserDetailsService实现。
2. 全局配置AuthenticationManager方式
参考官方文档:https://spring.io/guides/topicals/spring-security-architecture
package com.hx.demo.config;
/**
* @author Huathy
* @date 2023-02-27 21:22
* @description
*/
@Configuration
public class WebSecurityCfg extends WebSecurityConfigurerAdapter {
/**
* 写法一:
* springboot 对security默认配置 在工厂中默认创建 AuthenticationManager
*
* @param builder
*/
// @Autowired
// public void initialize(AuthenticationManagerBuilder builder) throws Exception {
// System.out.println("springboot 默认配置 builder = " + builder);
// // springboot 默认配置 builder = org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration$DefaultPasswordEncoderAuthenticationManagerBuilder@611e5819
// InMemoryUserDetailsManager userDetailsService = new InMemoryUserDetailsManager();
// 这里会对yml配置进行覆盖,配置为whx,明文密码123
// userDetailsService.createUser(User.withUsername("whx").password("{noop}123").roles("admin").build());
// builder.userDetailsService(userDetailsService);
// }
/**
* 写法二:
* 上面的写法也等同于这里的写法。SpringSecurity会自动检测代码中是否存在UserDetailsService。
* 如果有自定义的,AuthenticationManagerBuilder
*
* @return
*/
@Bean
public UserDetailsService myUserDetailsService() {
InMemoryUserDetailsManager userDetailsService = new InMemoryUserDetailsManager();
userDetailsService.createUser(User.withUsername("whx").password("{noop}123").roles("admin").build());
return userDetailsService;
}
/**
* 方法三:自定义AuthenticationManager
*
* @param builder
*/
@Override
public void configure(AuthenticationManagerBuilder builder) throws Exception {
System.out.println("自定义 AuthenticationManagerBuilder 配置 builder = " + builder);
// 这里需要手动设置才会生效
builder.userDetailsService(myUserDetailsService());
}
}
默认全局AuthenticationManager的总结:
- 默认自动配置创建全局AuthenticationManager默认找到当前项目中是否存在自定义UserDetailsService实例,自动将当前项目UserDetailsService实例设置为数据源。
- 默认自动配置创建全局AuthenticationManager在工厂中使用时在代码中注入即可。
自定义全局AuthenticationManager总结:
- 一旦通过configure方法自定义AuthenticationManager实现,就会将工厂中自动配置AuthenticationManager覆盖。
- 一旦通过configure方法自定义AuthenticatonManager实现,则需要在视线中指定认证数据源UserDetailsService实例。
- 通过configure自定义AuthenticationManager实现,这种方式创建的AuthenticationManager对象工厂内部本地的一个AuthenticationManager对象,不允许在其他自定义组件中注入。如果希望将本地的AuthenticationManager暴露给其他组件,需要在子类中调用父类方法。
/**
* 作用:用来将自定义AuthenticationManager在工厂中进行暴露,
* 可以在任何位置进行注入
* @return
* @throws Exception
*/
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
由于WebSecurityConfigurerAdapter过期,我们使用以下写法:
@EnableWebSecurity
@Slf4j
public class SecurityCfg2 {
// @Autowired
// public void initialize(AuthenticationManagerBuilder builder) throws Exception {
// System.out.println("springboot 默认配置 builder = " + builder);
// // springboot 默认配置 builder = org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration$DefaultPasswordEncoderAuthenticationManagerBuilder@611e5819
// InMemoryUserDetailsManager userDetailsService = new InMemoryUserDetailsManager();
// // 这里会对yml配置进行覆盖,配置为whx,明文密码123
// userDetailsService.createUser(User.withUsername("whx").password("{noop}123").roles("admin").build());
// builder.userDetailsService(userDetailsService);
// }
@Bean
public UserDetailsService myUserDetailsService() {
InMemoryUserDetailsManager userDetailsService = new InMemoryUserDetailsManager();
userDetailsService.createUser(User.withUsername("whx").password("{noop}123").roles("admin").build());
return userDetailsService;
}
}
3. 编码
3.1 创建数据库表与插入数据
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- Table structure for role
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`name_cn` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1004 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- Records of role
INSERT INTO `role` VALUES (1001, 'super_admin', '超级管理员');
INSERT INTO `role` VALUES (1002, 'sys_admin', '系统管理员');
INSERT INTO `role` VALUES (1003, 'user', '系统用户');
-- Table structure for user
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(255) CHARACTER SET latin1 COLLATE latin1_swedish_ci NULL DEFAULT NULL,
`password` varchar(255) CHARACTER SET latin1 COLLATE latin1_swedish_ci NULL DEFAULT NULL,
`accountNonExpired` int(1) NULL DEFAULT NULL,
`accountNunLocked` int(1) NULL DEFAULT NULL,
`credentialsNonExpired` int(1) NULL DEFAULT NULL,
`enable` int(1) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1004 CHARACTER SET = latin1 COLLATE = latin1_swedish_ci ROW_FORMAT = Dynamic;
-- Records of user
INSERT INTO `user` VALUES (1001, 'admin', '{noop}123', 1, 1, 1, 1);
INSERT INTO `user` VALUES (1002, 'huathy', '{noop}123', 1, 1, 1, 1);
INSERT INTO `user` VALUES (1003, 'dy', '{noop}123', 1, 1, 1, 1);
-- Table structure for user_role
DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`uid` int(11) NULL DEFAULT NULL,
`rid` int(11) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 10004 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- Records of user_role
INSERT INTO `user_role` VALUES (10001, 1001, 1001);
INSERT INTO `user_role` VALUES (10002, 1002, 1002);
INSERT INTO `user_role` VALUES (10003, 1003, 1003);
SET FOREIGN_KEY_CHECKS = 1;
3.2 创建实体类
@Data
@EqualsAndHashCode
public class User implements UserDetails {
private Integer id;
private String username;
private String password;
/**
* 账户是否过期
*/
private Boolean accountNonExpired;
/**
* 账户是否锁定
*/
private Boolean accountNunLocked;
/**
* 密码是否过期
*/
private Boolean credentialsNonExpired;
private Boolean enable;
private List<Role> roles = new ArrayList<>();
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Set<SimpleGrantedAuthority> authorities = new HashSet<>();
roles.forEach(role -> authorities.add(new SimpleGrantedAuthority("ROLE_"+role.getName())));
return authorities;
}
@Override
public boolean isAccountNonExpired() {
return accountNonExpired;
}
@Override
public boolean isAccountNonLocked() {
return accountNunLocked;
}
@Override
public boolean isCredentialsNonExpired() {
return credentialsNonExpired;
}
@Override
public boolean isEnabled() {
return enable;
}
}
3.3 导入Maven依赖与新增配置
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.22</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.48</version>
</dependency>
<dependency>
<groupId>com.enbatis</groupId>
<artifactId>mybatis-plugs-spring-boot-starter</artifactId>
<version>1.2.4</version>
</dependency>
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
druid:
url: jdbc:mysql://localhost:3306/demo?characterEncoding=UTF-8&useSSL=false
username: root
password: admin
driver-class-name: com.mysql.jdbc.Driver
mybatis:
mapper-locations: classpath://com.hx.mapper/**/*.xml
type-aliases-package: com.hx.entity
configuration:
map-underscore-to-camel-case: true
3.4 编写UserMapper
@Mapper
@Repository
public interface UserMapper extends BaseMapper<User> {
@Select(" select * from user t where t.username = #{username} limit 1")
User getUserByUname(String username);
@Select(" SELECT r.id,r.name,r.name_cn \n" +
" from `role` r \n" +
" left join user_role ur on r.id = ur.uid " +
" where ur.uid = #{uid} ")
List<Role> getRolesByUid(Integer uid);
}
3.5 编写MyUserDetailsService实现UserDetailsService接口
@Component
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userMapper.getUserByUname(username);
if (ObjectUtils.isEmpty(user)) {
throw new UsernameNotFoundException("用户名不正确");
}
List<Role> roles = userMapper.getRolesByUid(user.getId());
user.setRoles(roles);
return user;
}
}
二、前后端分离案例—认证总结
1. 编写LoginFilter、修改SecurityFilterChain
@EnableWebSecurity
@Slf4j
public class SecurityCfg2 {
@Autowired
private AuthenticationConfiguration authenticationConfiguration;
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
AuthenticationManager authenticationManager = authenticationConfiguration.getAuthenticationManager();
return authenticationManager;
}
@Bean
public LoginFilter loginFilter() throws Exception {
log.info(" === loginFilter init ===");
LoginFilter loginFilter = new LoginFilter();
// 指定接受json的用户名密码参数名称
loginFilter.setFilterProcessesUrl("/dologin");
loginFilter.setUsernameParameter("uname");
loginFilter.setPasswordParameter("pwd");
loginFilter.setAuthenticationManager(authenticationManagerBean());
loginFilter.setAuthenticationSuccessHandler((req, resp, authentication) -> {
Map<String, Object> resMap = new HashMap<>();
resMap.put("用户信息", authentication.getPrincipal());
resMap.put("authentication", authentication);
Result result = Result.success(resMap);
resp.setContentType("application/json;charset=UTF-8");
String jsonData = new ObjectMapper().writeValueAsString(result);
resp.setStatus(HttpStatus.OK.value());
resp.getWriter().write(jsonData);
});
loginFilter.setAuthenticationFailureHandler((req, resp, exception) -> {
Result result = Result.fail("登录失败", exception.getMessage());
resp.setContentType("application/json;charset=UTF-8");
String jsonData = new ObjectMapper().writeValueAsString(result);
resp.getWriter().write(jsonData);
});
return loginFilter;
}
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class);
http.authorizeHttpRequests()
.anyRequest().authenticated().and().formLogin();
http.exceptionHandling().authenticationEntryPoint((req, resp, exception) -> {
resp.setContentType(MediaType.APPLICATION_JSON_VALUE);
resp.setCharacterEncoding("UTF-8");
resp.setStatus(HttpStatus.UNAUTHORIZED.value());
resp.getWriter().println("请求未认证");
});
http.logout().logoutUrl("/logout").logoutSuccessHandler((req, resp, auth) -> {
Result result = Result.fail("注销成功", auth.getPrincipal());
resp.setContentType("application/json;charset=UTF-8");
String jsonData = new ObjectMapper().writeValueAsString(result);
resp.getWriter().write(jsonData);
});
http.csrf().disable();
// at:用当前过滤器来替换过滤器链中的哪个过滤器。before放在哪个过滤器之前,after放在哪个过滤器后
log.info(" === 替换了成了LoginFilter === ");
return http.build();
}
}
2. 自定义数据源
@Component
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userMapper.getUserByUname(username);
if (ObjectUtils.isEmpty(user)) {
throw new UsernameNotFoundException("用户名不正确");
}
List<Role> roles = userMapper.getRolesByUid(user.getId());
user.setRoles(roles);
return user;
}
}
三、前后端分离案例——添加验证码
1. 引入验证码依赖、编写验证码配置
<!-- 验证码生成 谷歌实现 -->
<dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<version>2.3.2</version>
</dependency>
@Configuration
public class KaptchaConfig {
@Bean
public Producer kaptcha() {
Properties properties = new Properties();
properties.setProperty("kaptcha.image.width", "150");
properties.setProperty("kaptcha.image.heigth", "50");
properties.setProperty("kaptcha.textproducer.char.string", "123456789");
properties.setProperty("kaptcha.textproducer.char.length", "4");
Config config = new Config(properties);
DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
defaultKaptcha.setConfig(config);
return defaultKaptcha;
}
}
2. 获取验证码接口
@RestController
public class VerifyCodeController {
@Autowired
private Producer producer;
@GetMapping("vc.jpg")
public String getVerifyCode(HttpSession session) throws IOException {
// 1. 生成验证码
String text = producer.createText();
// 2. 放入session或者redis
session.setAttribute("vcjpg", text);
// 3. 生成图片
BufferedImage image = producer.createImage(text);
// 4. 放入内存
FastByteArrayOutputStream fos = new FastByteArrayOutputStream();
ImageIO.write(image, "jpg", fos);
// 5. 返回base64
String img = Base64.getEncoder().encodeToString(fos.toByteArray());
return img;
}
}
3. 验证码校验filter
@Data
public class LoginVcFilter extends UsernamePasswordAuthenticationFilter {
public String FORM_VC_KEY = "verify_code";
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (!request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
try {
// 1. 获取请求验证码
Map<String, String> userInfo = new ObjectMapper().readValue(request.getInputStream(), Map.class);
String verifyCode = userInfo.get(getFORM_VC_KEY());
// 2. 获取session中的验证码
String vcjpg = String.valueOf(request.getSession().getAttribute("vcjpg"));
if (!vcjpg.equals(verifyCode)) {
throw new VerifyCodeException("验证码错误!");
}
// 3. 获取用户名和密码认证
String username = userInfo.get(getUsernameParameter());
String pwd = userInfo.get(getPasswordParameter());
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(username, pwd);
setDetails(request, authToken);
// 注意这里要调用authenticationManager中的auth方法
return this.getAuthenticationManager().authenticate(authToken);
} catch (IOException e) {
e.printStackTrace();
}
return super.attemptAuthentication(request, response);
}
}
4. 编写SpringSecurity配置类
@EnableWebSecurity
@Slf4j
public class SecurityConfig {
@Autowired
private AuthenticationConfiguration authenticationConfiguration;
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
AuthenticationManager authenticationManager = authenticationConfiguration.getAuthenticationManager();
return authenticationManager;
}
@Bean
public LoginVcFilter loginVcFilter() throws Exception {
log.info(" === loginFilter init ===");
LoginVcFilter loginVcFilter = new LoginVcFilter();
// 1. 指定认证接收参数
loginVcFilter.setPasswordParameter("pwd");
loginVcFilter.setUsernameParameter("uname");
loginVcFilter.setFORM_VC_KEY("code");
// 2. 指定认证处理URL
loginVcFilter.setFilterProcessesUrl("/dologin");
// 3. 指定认证管理器
loginVcFilter.setAuthenticationManager(authenticationManagerBean());
// 4. 指定成功处理
loginVcFilter.setAuthenticationSuccessHandler((req, resp, authentication) -> {
Map<String, Object> resMap = new HashMap<>();
resMap.put("用户信息", authentication.getPrincipal());
resMap.put("authentication", authentication);
Result result = Result.success(resMap);
resp.setContentType("application/json;charset=UTF-8");
String jsonData = new ObjectMapper().writeValueAsString(result);
resp.setStatus(HttpStatus.OK.value());
resp.getWriter().write(jsonData);
});
// 5. 指定失败处理
loginVcFilter.setAuthenticationFailureHandler((req, resp, exception) -> {
Result result = Result.fail("登录失败", exception.getMessage());
resp.setContentType("application/json;charset=UTF-8");
String jsonData = new ObjectMapper().writeValueAsString(result);
resp.getWriter().write(jsonData);
});
return loginVcFilter;
}
@Bean
protected SecurityFilterChain configure(HttpSecurity http) throws Exception {
log.info(" === 替换了 loginVcFilter === ");
http.addFilterAt(loginVcFilter(), UsernamePasswordAuthenticationFilter.class);
http.authorizeHttpRequests()
.mvcMatchers("/vc.jpg").permitAll()
.anyRequest().authenticated();
http.formLogin();
http.exceptionHandling().authenticationEntryPoint((req, resp, ex) -> {
resp.setContentType(MediaType.APPLICATION_JSON_VALUE);
resp.setStatus(HttpStatus.UNAUTHORIZED.value());
resp.getWriter().println("Please Visit After Login");
});
http.logout();
http.csrf().disable();
return http.build();
}
}