Directorio de artículos
- prefacio
- Versión de memoria (memoria)
- Versión de la base de datos (jdbc)
- Inicio de sesión personalizado - Único (custom-login-single)
- Inicio de sesión personalizado: parte delantera y trasera separadas
- Cambiar inicio de sesión a inicio de sesión json (inicio de sesión personalizado-json)
- certificado
- Autorización/Control de acceso (custom-auth-control)
- Dirección del almacén
prefacio
Como primer artículo, este artículo usará ejemplos para ilustrar el uso de Spring Security en producción y expandir sus funciones. Cada solución tendrá un código de ejemplo completo, y el almacén de códigos se publicará al final del artículo.
La teoría involucrada en este artículo es menor, principalmente con ejemplos.
Versión de memoria (memoria)
Esta versión no tiene tecnología y se puede utilizar introduciendo dependencias.
importación directa
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
- Después de introducir las dependencias, puede iniciar el proyecto directamente
- Se generará una contraseña aleatoria en la consola y el nombre de usuario predeterminado es
user
Usando la contraseña de seguridad generada: 2233be62-e65f-489b-a52c-4bba21bcfd14
-
establecer contraseña de usuario
-
Puede configurarse a través de application.yml, o inyectarse a través de config, obviamente es más conveniente usar yml
spring: security: # 配置默认的 InMemoryUserDetailsManager 的用户账号与密码。 user: name: ali password: 123456 roles: admin
-
Hay dos formas de escribir directamente a través del código, y las dos también se pueden mezclar
Método 1: inyección directa
InMemoryUserDetailsManager
@Configuration @EnableWebSecurity public class SecurityWebCofnig extends WebSecurityConfigurerAdapter { @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() .withUser("ali") .password(passwordEncoder().encode("123456")) .roles("admin"); } }
Método 2: método orientado a la interfaz
@Configuration @EnableWebSecurity public class SecurityWebCofnig extends WebSecurityConfigurerAdapter { @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Autowired private UserDetailsService userDetailsService; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() .withUser("ali") .password(passwordEncoder().encode("123456")) .roles("admin"); auth.userDetailsService(userDetailsService); } } @Component public class CustomUserServiceImpl implements UserDetailsService { @Autowired private PasswordEncoder passwordEncoder; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { return new User("ali2", passwordEncoder.encode("123456"), AuthorityUtils.commaSeparatedStringToAuthorityList("admin")); } }
-
Versión de la base de datos (jdbc)
Esta versión, basada en la versión de memoria, ha mejorado la función de verificación del usuario, extendiéndose desde el usuario integrado hasta la base de datos, y también es una versión relativamente limitada.
Luego, el enfoque aquí es el userDetailService
método de implementación, UserDetail
solo devuelva una subclase, y otra autenticación de inicio de sesión y redirección se realizan por seguridad.
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
@Data
@TableName("role")
public class Role implements Serializable {
private static final long serialVersionUID = -27787294406430777L;
@TableField("id")
private Integer id;
@TableField("name")
private String name;
@TableField("code")
private String code;
@TableField("status")
private Integer status;
@TableField("deleted")
private Integer deleted;
}
@Data
@TableName("user")
public class SysUser {
private String id;
private String username;
private String realName;
private String password;
@TableField(exist = false)
private String[] roles;
}
@Data
@TableName("user_role")
public class UserRole implements Serializable {
private static final long serialVersionUID = 180339547857105479L;
@TableField("id")
private Integer id;
@TableField("role_id")
private Integer roleId;
@TableField("user_id")
private Integer userId;
}
public interface SysUserService {
SysUser getByUsername(String username);
}
@Service
@AllArgsConstructor
public class SysUserServiceImpl implements SysUserService {
private SysUserRepository sysUserRepository;
private UserRoleRepository userRoleRepository;
private RoleRepository roleRepository;
@Override
public SysUser getByUsername(String username) {
SysUser user = sysUserRepository.getByUsername(username);
if (user == null) {
return null;
}
List<UserRole> roles = userRoleRepository.getByUserId(user.getId());
if (roles.isEmpty()) {
return user;
}
List<Integer> roleIds = roles.stream().map(UserRole::getRoleId).collect(Collectors.toList());
user.setRoles(roleRepository.listByIds(roleIds).stream().map(Role::getCode).collect(Collectors.toList()));
return user;
}
}
Método de consulta de seguridad definido por el usuario
@Component
@AllArgsConstructor
public class CustomUserServiceImpl implements UserDetailsService {
private SysUserService sysUserService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser sysUser = sysUserService.getByUsername(username);
if (sysUser == null) {
return null;
}
return new User(sysUser.getUsername(), sysUser.getPassword(), AuthorityUtils.createAuthorityList(sysUser.getRoles()));
}
}
Al inyectar aquí, se retienen los usuarios de la versión de memoria.
@Configuration
@EnableWebSecurity
public class SecurityWebConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("ali")
.password(passwordEncoder().encode("123456"))
.roles("admin");
auth.userDetailsService(userDetailsService);
}
}
Yo uso Mybatis plus, agregue mapperScan aquí
@MapperScan("com.liry.security.repository.mapper")
@SpringBootApplication
public class JdbcApp {
public static void main(String[] args) {
SpringApplication.run(JdbcApp.class);
}
}
aplicación.yml
spring:
datasource:
url: jdbc:mysql://localhost:3306/ali?useSSl=false&zeroDateTimeBehavior=CONVERT_TO_NULL&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
minimum-idle: 5
idle-timeout: 600000
maximum-pool-size: 10
auto-commit: true
pool-name: SsoHikariCP
max-lifetime: 1800000
connection-timeout: 30000
connection-test-query: SELECT 1
main:
allow-bean-definition-overriding: true
mybatis-plus:
global-config:
db-config:
logic-delete-field: deleted # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
id-type: AUTO
configuration:
# 日志打印
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
Inicio de sesión personalizado - Único (custom-login-single)
Esta versión actualiza la función personalizada de inicio de sesión en la versión de la base de datos, y puede configurar su propia página de inicio de sesión, página de inicio, API de procesamiento de inicio de sesión y procesamiento de devolución de llamada para inicios de sesión exitosos y fallidos según el proyecto;
Digresión: la ventaja de no separar el anverso y el reverso es un desarrollo rápido y una implementación fácil, pero sus desventajas también son obvias. Con el proyecto iterativo, se volverá cada vez más grande. La corriente principal todavía está distribuida, y los proyectos individuales son raros ahora.
Así que aquí están las cosas a tener en cuenta:
- Después de introducir el marco frontal, debe configurarse en application.yml (yo uso themleaf aquí)
- La clase de herencia
WebSecurityConfigurerAdapter
completa la configuración personalizada- Use la configuración para una página de inicio de sesión exitosa
defaultSuccessUrl
, no usesuccessForwardUrl
- Puede definir un posprocesador exitoso y un procesador fallido, estos dos serán llamados después de la autenticación y puede hacer una expansión comercial
- Use la configuración para una página de inicio de sesión exitosa
- La configuración
loginProcessingUrl
no es para transferir la solicitud de procesamiento a la interfaz que definió, sino para modificar la dirección API dentro de la seguridad.
Aviso:
loginProcessingUrl
: la api para el procesamiento de inicio de sesión, esto es solo para definir la api, no para cambiar la autenticación a personalizada o para ser controlado por seguridad
defaultSuccessUrl
: La dirección de salto después de un inicio de sesión exitoso, hay otra successForwardUrl
, pero solo se puede usar defaultSuccessUrl
, y successForwardUrl
aparecerá un error 405 al usarla
permitAll
: permitir todos los métodos de solicitud
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
@Override
protected void configure(HttpSecurity http) throws Exception {
// 表单
http
.formLogin()
// 自定义登录页
.loginPage("/login.html")
// 登录的请求接口,对应表单的action,这里只是修改了处理的api,实际处理还是security
.loginProcessingUrl("/loginDeal")
// 不能写:successForwardUrl("/index.html"),会报405
.defaultSuccessUrl("/index.html")
// 登录失败转发到哪个页面
.failureForwardUrl("/login.html?error=true")
// 登录的用户名和密码参数名称
.usernameParameter("username")
.passwordParameter("pwd")
.successHandler(new LoginSuccessHandler())
.failureHandler(new LoginFailureHandler())
.permitAll()
// 开启认证
.and().authorizeRequests()
//设置哪些路径可以直接访问,不需要认证
.antMatchers("/test/*").permitAll()
//需要认证
.anyRequest().authenticated()
.and().csrf().disable(); //关闭csrf防护
}
Devolución de llamada de error de inicio de sesión
public class LoginFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,
AuthenticationException e) throws IOException, ServletException {
// 处理失败的后置操作
}
}
Devolución de llamada de inicio de sesión exitosa
public class LoginSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,
Authentication authentication) throws IOException, ServletException {
// 处理成功的一个后置操作
}
}
controlador de prueba
@Controller
public class LoginController {
@GetMapping("/login.html")
public String loginPage() {
return "login";
}
@GetMapping("/index.html")
public String index() {
return "index";
}
}
Página de inicio de sesión probada (login.html)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录</title>
</head>
<body>
<div>
<!-- 这里action对应的是在-->
<form action="/loginDeal" method="POST" >
用户名<input placeholder="输入用户名" name="username"></br>
密 码<input placeholder="输入密码" name="pwd" type="password"></br>
<button type="submit">登录</button>
</form>
</div>
</body>
</html>
página de inicio de prueba
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>首页</title>
</head>
<body>
<h2>首页</h2>
</body>
</html>
Inicio de sesión personalizado: parte delantera y trasera separadas
A diferencia de un solo proyecto, la escalabilidad de la separación de adelante hacia atrás aumenta considerablemente, lo que significa que enfrentará más problemas, como la consistencia entre dominios y sesiones.
-
Personaliza el método
CustomUserServiceImpl
de carga deUserDetailsService
la información del usuario -
Método de procesamiento de devolución de llamada de autenticación personalizado
LoginSuccessHandler/LoginFailureHandler
(inicio de sesión exitoso, almacenar información de usuario, serialización de respuesta) -
Crear una
CacheManager
caché unificada de interfaz de caché -
heredar
WebSecurityConfigurerAdapter
Configuración:
corsFilter(跨域)
userDetailService(用户信息)
passwordEncoder(加解密)
securityContextRepository(认证信息管理)
AuthenticationEntryPoint(响应序列号)
-
Hay muchas maneras de lidiar con la consistencia de la sesión. Diferentes personas tienen diferentes opiniones. Hay ventajas y desventajas. Redis se usa aquí. Si no hay necesidad de un entorno grande, también son posibles un front-end y un back-end.
Gestión de caché, redis utilizada aquí;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.setKeySerializer(new StringRedisSerializer());
return redisTemplate;
}
}
/**
* @author ALI
* @since 2023/6/4
*/
@Component
public class CacheManager {
private static final int TIME_OUT = 4;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
public Object get(String key) {
return redisTemplate.opsForValue().get(key);
}
public <T> T get(String key, Class<T> clazz) {
Object value = get(key);
if (value == null) {
return null;
}
return (T) value;
}
public void set(String key, Object value) {
if (value == null) {
redisTemplate.delete(key);
return;
}
redisTemplate.opsForValue().set(key, value, TIME_OUT, TimeUnit.HOURS);
}
public void set(String key, Object value, Long timeOut, TimeUnit timeUnit) {
if (value == null) {
redisTemplate.delete(key);
return;
}
redisTemplate.opsForValue().set(key, value, timeOut, timeUnit);
}
public boolean containsKey(String key) {
return Boolean.TRUE.equals(redisTemplate.hasKey(key));
}
public long getExpire(String key) {
Long expire = redisTemplate.getExpire(key);
if (expire == null) {
return 0L;
}
return expire;
}
}
/**
* 认证的常量
* @author ALI
* @since 2023/6/10
*/
public class AuthConstant {
public static final String LOGIN_PRE = "login:";
public static final String CAPTCHA_PRE = "captcha:";
public static String buildLoginKey(String key) {
return LOGIN_PRE + key;
}
public static String buildCaptchaKey(String key) {
return CAPTCHA_PRE + key;
}
}
@Data
public class CustomUser implements UserDetails {
private static final long serialVersionUID = 5469888959861441262L;
protected String userId;
protected String password;
protected String username;
protected Collection<? extends GrantedAuthority> authorities;
public CustomUser() {
}
public CustomUser(SysUser sysUser) {
this.userId = sysUser.getId();
this.username = sysUser.getUsername();
this.password = sysUser.getPassword();
if (!CollectionUtils.isEmpty(sysUser.getRoles())) {
this.authorities = sysUser.getRoles().stream().map(d -> new SimpleGrantedAuthority("ROLE_" + d)).collect(Collectors.toList());
}
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
objeto de respuesta
/**
* 返回结果对象
*
* @author 李瑞益
* @since 2019/9/25
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ResponseData<T> implements Serializable {
public static final String SUCCESS = "success";
public static final String FAILED = "failed";
private static final long serialVersionUID = -4304353934293881342L;
/** 处理结果 */
private boolean status;
/** 信息 */
private String message;
/** 状态编码 */
private String code;
/** 数据对象 */
private T data;
public ResponseData(boolean status, T data) {
this.status = status;
this.data = data;
this.code = status ? SUCCESS : FAILED;
}
public ResponseData(boolean status, T data, String message) {
this.status = status;
this.data = data;
this.message = message;
this.code = status ? SUCCESS : FAILED;
}
public ResponseData(Throwable e) {
this.status = false;
this.message = e.getMessage();
this.code = FAILED;
}
public static ResponseData<Object> ok() {
return new ResponseData<>(true, null);
}
public static ResponseData<Object> failed() {
return new ResponseData<>(false, null);
}
public static ResponseData<Object> failed(Throwable e) {
return new ResponseData<>(false, null, e.getMessage());
}
public static ResponseData<Object> failed(String message) {
return new ResponseData<>(false, null, message);
}
}
/**
* 主要用来做响应体序列化
* @author ALI
* @since 2023/6/4
*/
public class CustomizeAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
httpServletResponse.setContentType("application/json;charset=utf-8");
httpServletResponse.setStatus(HttpStatus.UNAUTHORIZED.value());
ResponseData<Object> result = ResponseData.failed(e.getMessage());
PrintWriter out = httpServletResponse.getWriter();
out.write(JSON.toJSONString(result));
out.flush();
out.close();
}
}
Tenga en cuenta que:
Nuestros códigos anteriores son todos servicios basados en una sola arquitectura, por lo que al configurar SecurityWebConfig
, configuraremos páginas de inicio de sesión, páginas de error, etc., pero estas cosas ya no son necesarias después de separar el anverso y el reverso.
Esquema de consistencia de sesión
Cuando se trata del esquema de consistencia de la sesión, aquí hay tres esquemas que se desarrollan gradualmente.
opcion uno
Utilice la memoria caché redis directamente en el método de devolución de llamada (inicio de sesión personalizado)
Esta solución utiliza el procesador de devolución de llamada de inicio de sesión exitoso en seguridad para establecer la información del usuario en redis, luego agrega un interceptor de encabezado de solicitud para interceptar el token en el encabezado de la solicitud, obtiene la información del usuario en redis a través del token y luego lo establece en el contexto de seguridad SecurityContextHolder
Eso está bien; la desventaja de esta solución es que mantendrá dos cachés, y la cantidad de código también es grande
- Personaliza el método
CustomUserServiceImpl
de carga deUserDetailsService
la información del usuario - Método de procesamiento de devolución de llamada de autenticación personalizado
LoginSuccessHandler/LoginFailureHandler
(inicio de sesión exitoso, almacenar información de usuario, serialización de respuesta) - Crear una
CacheManager
caché unificada de interfaz de caché CustomizeAuthenticationEntryPoint
Implementación personalizada delAuthenticationEntryPoint
método de serialización.CustomHeaderAuthFilter
Herencia personalizadaBasicAuthenticationFilter
para completar el procesamiento del token de encabezado de solicitudLoginSuccessHandler
Agregar lógica de almacenamiento en caché del usuario- Heredar
WebSecurityConfigurerAdapter
configuración inicio de sesión personalizado
Aviso:
-
Si lo usa , no lo establezca al
BasicAuthenticationFilter
configurar , o no pasará por nuestro filtro personalizadoWebSecurityConfigurerAdapter
http.httpBasic()
-
Este método estará separado de la seguridad. En la personalización
CustomHeaderAuthFilter
, debe juzgar la API de inicio de sesión e ignorar la API para evitar ser interceptado por usted mismo.
LoginSuccessHandler: después de que el usuario inicie sesión correctamente, configure la información del usuario en el caché y serialice json en el front-end
/**
* 登录成功处理器
* 序列化处理
*
* @author ALI
* @since 2023/6/1
*/
public class LoginSuccessHandler implements AuthenticationSuccessHandler {
private CacheManager cacheManager;
public LoginSuccessHandler(CacheManager cacheManager) {
this.cacheManager = cacheManager;
}
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,
Authentication authentication) throws IOException, ServletException {
// 将成功后的会话id设置到响应,以便在链里的过滤器能够拿到
String token = UUID.randomUUID().toString();
httpServletResponse.setHeader(HttpHeaders.AUTHORIZATION, token);
// 设置响应的格式
httpServletResponse.setContentType("application/json;charset=utf-8");
ResponseData<CustomUser> result = new ResponseData<>();
CustomUser user = (CustomUser) authentication.getPrincipal();
cacheManager.set(AuthConstant.buildLoginKey(token), user);
UserView temp = new UserView(user, token);
result.setData(temp);
PrintWriter writer = httpServletResponse.getWriter();
writer.write(JSON.toJSONString(result));
writer.flush();
}
}
CustomHeaderAuthFilter: un filtro de encabezado de solicitud personalizado que intercepta el encabezado del token en el encabezado de la solicitud. Esto es equivalente a nuestra propia iniciativa para autenticar. Luego, este filtro debe estar antes del filtro. Después de la autenticación, debe pasar la autenticación. La información se establece UsernamePasswordAuthenticationFilter
en el contexto de seguridad, y debido a que nuestro filtro es un poco independiente de la seguridad, necesitamos sincronizar la SecurityWebConfig
API ignorada configurada en él;
Cabe señalar que todos ellos try catch
se capturan aquí, por lo que cualquier excepción ExceptionTranslationFilter
será manejada por
/**
* 主要用来拦截token的
* <p>
* 这里构造器我需要注入authenticationManager ,但是这个类在SecurityConfig里注入,所有我只有在用到的地方手动注入
*
* @author ALI
* @since 2023/6/4
*/
public class CustomHeaderAuthFilter extends BasicAuthenticationFilter {
private CacheManager cacheManager;
public CustomHeaderAuthFilter(AuthenticationManager authenticationManager,
AuthenticationEntryPoint authenticationEntryPoint, CacheManager cacheManager) {
super(authenticationManager, authenticationEntryPoint);
this.cacheManager = cacheManager;
}
private void doParse(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
String header = request.getHeader(HttpHeaders.AUTHORIZATION);
try {
// 这个步骤是将redis的信息设置到security上下文
if (header.startsWith("Bearer")) {
String token = header.replace("Bearer ", "");
CustomUser user = cacheManager.get(AuthConstant.buildLoginKey(token), CustomUser.class);
if (user == null) {
throw new AccountExpiredException("token无效!");
}
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(user, user.getPassword(), user.getAuthorities());
// 设置的上下文
SecurityContextHolder.getContext().setAuthentication(authToken);
}
} catch (Exception e) {
// 抛出 AuthenticationException AccessDeniedException 两个类型的异常给 ExceptionTranslationFilter
throw new AccountExpiredException("登录失败!");
}
chain.doFilter(request, response);
}
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,
FilterChain filterChain) throws ServletException, IOException {
String authorization = httpServletRequest.getHeader(HttpHeaders.AUTHORIZATION);
// 无token,和登录的走默认的逻辑
// 还有被忽略的api
if (httpServletRequest.getRequestURI().contains("/login") || httpServletRequest.getRequestURI().contains("/test")) {
filterChain.doFilter(httpServletRequest, httpServletResponse);
return;
}
this.doParse(httpServletRequest, httpServletResponse, filterChain);
}
}
SecurityWebConfig: Aquí está la configuración de seguridad, cabe señalar que no olvide inyectar la clase personalizada y configurarla
/**
* security认证配置
*
* @author ALI
* @since 2023/5/29
*/
@Configuration
@EnableWebSecurity
public class SecurityWebConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private CacheManager cacheManager;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public org.springframework.web.filter.CorsFilter corsFilter() {
return new CorsFilter(corsConfigurationSource());
}
/**
* 跨域设置
*/
private CorsConfigurationSource corsConfigurationSource() {
org.springframework.web.cors.UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
// 允许cookies跨域
config.setAllowCredentials(true);
// 允许向该服务器提交请求的URI,* 表示全部允许,在SpringMVC中,如果设成*,会自动转成当前请求头中的Origin
config.addAllowedOrigin("*");
// 允许访问的头信息,* 表示全部
config.addAllowedHeader("*");
// 预检请求的缓存时间(秒),即在这个时间段里,对于相同的跨域请求不会再预检了
config.setMaxAge(18000L);
// 允许提交请求的方法,* 表示全部允许
config.addAllowedMethod("GET");
config.addAllowedMethod("PUT");
config.addAllowedMethod("POST");
config.addAllowedMethod("DELETE");
source.registerCorsConfiguration("/**", config);
return source;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("ali")
.password(passwordEncoder().encode("123456"))
.roles("admin");
auth.userDetailsService(userDetailsService);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 表单
// 注意:使用了`BasicAuthenticationFilter`那么在配置`WebSecurityConfigurerAdapter`时,就不要设置`http.httpBasic()`,不然不会走我们自定义的Filter
http
.formLogin()
// 登录的请求接口,对应表单的action,这里只是修改了处理的api,实际处理还是security
.loginProcessingUrl("/loginDeal")
// 登录的用户名和密码参数名称
.usernameParameter("username")
.passwordParameter("pwd")
.successHandler(new LoginSuccessHandler(cacheManager))
.failureHandler(new LoginFailureHandler())
.permitAll()
// 开启认证
.and().authorizeRequests()
//设置哪些路径可以直接访问,不需要认证
.antMatchers("/test/*").permitAll()
//需要认证
.anyRequest().authenticated()
.and().csrf().disable(); //关闭csrf防护
http.exceptionHandling().authenticationEntryPoint(new CustomizeAuthenticationEntryPoint());
http.addFilterBefore(
new CustomHeaderAuthFilter(authenticationManager(), new CustomizeAuthenticationEntryPoint(), cacheManager),
UsernamePasswordAuthenticationFilter.class);
}
}
Opción II
Vuelva a escribir la clase de proveedor de caché dentro de Spring Security SecurityContextRepository
(custom-login2)
Esta solución parte de la perspectiva del marco de código fuente, y la ejecución de reemplazo bean
ha logrado que el acceso final a la información del usuario sea el mismo espacio de caché.Es muy simple en principio, pero requiere algunos conocimientos de código fuente y los requisitos comerciales. son cambiables Aunque admite security
la expansión, pero el costo de aprendizaje no es bajo, por lo que debe mirar el proyecto desde una perspectiva global, pero es muy cómodo de usar y la cantidad de código es muy pequeña, si desea Comprueba el uso interno, puedes comprobarlo SecurityContextPersistenceFilter
.
- Personaliza el método
CustomUserServiceImpl
de carga deUserDetailsService
la información del usuario - Método de procesamiento de devolución de llamada de autenticación personalizada
LoginSuccessHandler/LoginFailureHandler
(serialización de respuesta) - Crear una
CacheManager
caché unificada de interfaz de caché CustomizeAuthenticationEntryPoint
Implementación personalizada delAuthenticationEntryPoint
método de serialización.CustomSecurityContextRepository
La herencia personalizadaSecurityContextRepository
anula sus 3 métodos de acceso a la información- Heredar
WebSecurityConfigurerAdapter
configuración inicio de sesión personalizado
/**
* 自定义的session存储器
*
* @author ALI
* @since 2023/6/4
*/
@Component
public class CustomSecurityContextRepository implements SecurityContextRepository {
@Autowired
private CacheManager cacheManager;
@Override
public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) {
HttpServletRequest request = requestResponseHolder.getRequest();
String token = request.getHeader(HttpHeaders.AUTHORIZATION);
if (StringUtils.isBlank(token)) {
return generateNewContext();
}
token = token.replace("Bearer ", "");
SecurityContextImpl s = cacheManager.get(AuthConstant.buildLoginKey(token), SecurityContextImpl.class);
if (s == null) {
return generateNewContext();
}
return s;
}
@Override
public void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response) {
String token = request.getHeader(HttpHeaders.AUTHORIZATION);
if (StringUtils.isBlank(token)) {
token = response.getHeader(HttpHeaders.AUTHORIZATION);
}
if (StringUtils.isBlank(token)) {
return;
}
token = token.replace("Bearer ", "");
// 登录成功和失败的回调(LoginSuccessHandler,LoginFailureHandler)是在UsernamePasswordAuthenticationFilter过滤器里执行的
// 而这里的认证信息缓存是在`SecurityContextPersistenceFilter`的doFilter后执行的
// `SecurityContextPersistenceFilter`的顺序比`UsernamePasswordAuthenticationFilter`的顺序小,
// 那么doFilter之后的方法就晚与LoginSuccessHandler,LoginFailureHandler
cacheManager.set(AuthConstant.buildLoginKey(token), context);
}
@Override
public boolean containsContext(HttpServletRequest request) {
String token = request.getHeader(HttpHeaders.AUTHORIZATION);
if (StringUtils.isBlank(token)) {
return false;
}
token = token.replace("Bearer ", "");
return cacheManager.containsKey(AuthConstant.buildLoginKey(token));
}
protected SecurityContext generateNewContext() {
return SecurityContextHolder.createEmptyContext();
}
}
configuración de seguridad
@Configuration
@EnableWebSecurity
public class SecurityWebConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private CustomSecurityContextRepository securityContextRepository;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public org.springframework.web.filter.CorsFilter corsFilter() {
return new CorsFilter(corsConfigurationSource());
}
/**
* 跨域设置
*/
private CorsConfigurationSource corsConfigurationSource() {
org.springframework.web.cors.UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
// 允许cookies跨域
config.setAllowCredentials(true);
// 允许向该服务器提交请求的URI,* 表示全部允许,在SpringMVC中,如果设成*,会自动转成当前请求头中的Origin
config.addAllowedOrigin("*");
// 允许访问的头信息,* 表示全部
config.addAllowedHeader("*");
// 预检请求的缓存时间(秒),即在这个时间段里,对于相同的跨域请求不会再预检了
config.setMaxAge(18000L);
// 允许提交请求的方法,* 表示全部允许
config.addAllowedMethod("GET");
config.addAllowedMethod("PUT");
config.addAllowedMethod("POST");
config.addAllowedMethod("DELETE");
source.registerCorsConfiguration("/**", config);
return source;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("ali")
.password(passwordEncoder().encode("123456"))
.roles("admin");
auth.userDetailsService(userDetailsService);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 表单
// 注意:使用了`BasicAuthenticationFilter`那么在配置`WebSecurityConfigurerAdapter`时,就不要设置`http.httpBasic()`,不然不会走我们自定义的Filter
http
.formLogin()
// 登录的请求接口,对应表单的action,这里只是修改了处理的api,实际处理还是security
.loginProcessingUrl("/loginDeal")
// 登录的用户名和密码参数名称
.usernameParameter("username")
.passwordParameter("pwd")
.successHandler(new LoginSuccessHandler())
.failureHandler(new LoginFailureHandler())
.permitAll()
// 开启认证
.and().authorizeRequests()
//设置哪些路径可以直接访问,不需要认证
.antMatchers("/test/*").permitAll()
//需要认证
.anyRequest().authenticated()
.and().csrf().disable(); //关闭csrf防护
// 将我们的repository设置到共享变量里
http.setSharedObject(SecurityContextRepository.class, securityContextRepository);
http.exceptionHandling().authenticationEntryPoint(new CustomizeAuthenticationEntryPoint());
}
}
¿Por qué usar esto: http.setSharedObject
?
Mire SecurityContextConfigurer
la clase de configuración, obtendrá http.getSharedObject(SecurityContextRepository.class)
la clase de implementación correspondiente, que Map implementa internamente, por lo que cuando configuramos http.setSharedObject
nuestro repositorio personalizado, podemos reescribirlo; de manera predeterminada, http.getSharedObject(SecurityContextRepository.class) = null
se new HttpSessionSecurityContextRepository
establece directamente enSecurityContextPersistenceFilter
tercera solucion
uso Spring session
(inicio de sesión personalizado3)
Esta solución utiliza componentes de primavera spring session
, que también es una primavera nativa, y también está dirigida a la gestión de sesiones; nuevamente, diferentes personas tienen diferentes opiniones.
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
y luego anotar@SpringBootApplication
Entonces está hecho. Este método ahorra el mayor esfuerzo. Después de todo, este es un componente producido por Spring. Es una actualización funcional para Spring Framework. Este método no requiere que configure tokens y otras cosas.
¿Por qué no usar JWT?
De hecho, hay otra forma de resolver la consistencia de la sesión, pero realmente no apoyo esta forma, es decir, se JWT
define como un inicio de sesión sin estado, y su significado principal es que la JWT
información generada es la información de inicio de sesión, por lo que mientras Como con esta información, los datos de inicio de sesión se pueden JWT
obtener a través de esta información.
Es muy bueno, pero hay algunas cosas que no me gustan mucho:
- Hay una parte de la información generada por JWT que se puede revertir para extraer información del usuario;
- JWT no puede caducar activamente, por lo que no se puede iniciar en el verdadero sentido, luego, para lograr la caducidad, debe almacenarse en caché, entonces esto es lo mismo que la segunda solución, pero la segunda solución no necesita escribir el descifrado, verificar la legitimidad de el JWT, y no necesita ir Adaptar la seguridad;
- Esto es similar a lo anterior, es decir, si la empresa requiere restricciones de inicio de sesión, como solo un inicio de sesión con la misma cuenta, JWT aún debe almacenarse en caché;
Los 3 puntos anteriores son suficientes para que no admita el uso de JWT.
Cambiar inicio de sesión a inicio de sesión json (inicio de sesión personalizado-json)
El inicio de sesión predeterminado de seguridad es all POST + form-data
. Si usa json, no puede obtener los parámetros, pero los proyectos front-end generalmente realizan un procesamiento de intercepción unificado. Por supuesto, también puede dejar que el front-end cambie el inicio de sesión a formData request.
Hay varios puntos a tener en cuenta sobre este inicio de sesión:
-
La autenticación de inicio de sesión predeterminada
UsernamePasswordAuthenticationFilter
se realiza mediante , su valor no está en formato json, por lo que reescribimos su método de autenticaciónattemptAuthentication
; -
Establezca nuestra
UsernamePasswordAuthenticationFilter
configuración personalizada enHttpSecurity
, porque es un reemplazo, por lo que debemosUsernamePasswordAuthenticationFilter
copiar la configuración relevante, como el nombre del parámetro de inicio de sesión, la API de procesamiento de inicio de sesión, el procesador de éxito de inicio de sesión y el procesador de fallas ( esto es muy importante ); -
PasswordEncoder
La coincidencia de contraseñas consiste en pasar contraseñas sin cifrar (pasadas por el front-end) y cifradas (guardadas por el back-end), por lo que si la contraseña está cifrada, debe descifrarse aquí;
Personaliza UsernamePasswordAuthenticationFilter
la autenticación de json
/**
* 自定义json认证
* @author ALI
* @since 2023/6/7
*/
public class CustomUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
try (InputStream inputStream = request.getInputStream()) {
ObjectMapper objectMapper = new ObjectMapper();
Map<String, String> loginRequest = objectMapper.readValue(inputStream, Map.class);
String username = loginRequest.get(super.getUsernameParameter());
String password = loginRequest.get(super.getPasswordParameter());
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
return this.getAuthenticationManager().authenticate(authenticationToken);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
Agregar configuración personalizadaHttpSecurity
@Override
protected void configure(HttpSecurity http) throws Exception {
// 表单
// 注意:使用了`BasicAuthenticationFilter`那么在配置`WebSecurityConfigurerAdapter`时,就不要设置`http.httpBasic()`,不然不会走我们自定义的Filter
http
// 开启认证
.authorizeRequests()
//设置哪些路径可以直接访问,不需要认证
.antMatchers("/test/*").permitAll()
//需要认证
.anyRequest().authenticated()
.and().csrf().disable(); //关闭csrf防护
// 将我们的repository设置到共享变量里
http.setSharedObject(SecurityContextRepository.class, securityContextRepository);
// 设置序列化
http.exceptionHandling().authenticationEntryPoint(new CustomizeAuthenticationEntryPoint());
// 将我们自定义过滤器加入到原来UsernamePasswordAuthenticationFilter的前面
http.addFilterBefore(customUsernamePasswordAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}
/**
* 自定义的customUsernamePasswordAuthenticationFilter
* 需要同步在HttpSecurity里的配置
*/
public CustomUsernamePasswordAuthenticationFilter customUsernamePasswordAuthenticationFilter() throws Exception {
CustomUsernamePasswordAuthenticationFilter result = new CustomUsernamePasswordAuthenticationFilter();
result.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher(loginApi));
result.setAuthenticationManager(this.authenticationManager());
result.setUsernameParameter(usernameParameter);
result.setPasswordParameter(passwordParameter);
result.setAuthenticationSuccessHandler(new LoginSuccessHandler());
result.setAuthenticationFailureHandler(new LoginFailureHandler());
return result;
}
Entonces addFilterBefore
este método nos lo proporciona la seguridad para expandirnos, si personalizamos uno UsernamePasswordAuthenticationFilter
y lo ponemos al frente, después de que se pase nuestra autenticación, no seguiremos pasando por los siguientes filtros, y nuestra cobertura está completa.
certificado
Aquí, el proyecto custom-login-json se usa como proyecto básico.
Porque si desea atacar su sitio web, la primera posibilidad es intentar descifrar la contraseña. Si su transmisión no es segura, será interceptada, por lo que debe encriptarla. Además, si el atacante usa fuerza bruta para atacar, la contraseña requiere Debe ser complicado, aumentar el rango de agotamiento del atacante, reducir la tasa de aciertos y aumentar la cantidad de fallas de la cuenta, así como el límite de ip. Al mismo tiempo, también puede configurar el código de verificación para aumentar la dificultad de craqueo.
Inicio de sesión cifrado con contraseña (autorización personalizada)
La seguridad de la red también es algo que nuestros programadores deben considerar, así que aquí hacemos un cifrado y descifrado de contraseñas, aquí usamos el segundo esquema (custom-login2) como proyecto básico, pero el siguiente esquema no se limita a un determinado esquema, sino Es aplicable a todos los elementos de seguridad.
herramienta de cifrado
Aquí se utiliza el algoritmo de cifrado asimétrico RSA, y tanto el cifrado como el descifrado requieren el uso de claves públicas y secretas, lo cual es muy seguro.
/**
* rsa加密工具简化版
*
* @author ALI
* @date 2021-09-25 15:44
*/
public class RsaUtil {
private static final String RSA_ALGORITHM = "RSA";
private static final String AES_ALGORITHM = "AES";
private RsaUtil() {
}
/**
* 生成密钥对
*
* @param passKey 关键密码
* @return 密钥对
*/
public static KeyPair genratorKeyPair(String passKey) throws NoSuchAlgorithmException {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(RSA_ALGORITHM);
SecureRandom secureRandom = new SecureRandom(passKey.getBytes());
secureRandom.setSeed(passKey.getBytes());
keyPairGenerator.initialize(2048, secureRandom);
return keyPairGenerator.generateKeyPair();
}
/**
* 加密密码
*
* @param password 密码
* @param publicKey 公钥
* @return 加密后的密文
*/
public static byte[] encrypt(PublicKey publicKey, byte[] password) {
try {
Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
return cipher.doFinal(password);
} catch (Exception e) {
throw new RuntimeException("RSA加密失败(RSA encrypt failed.)");
}
}
/**
* 解密密码
*
* @param encryptPassword 加密的密码
* @param privateKey 私钥
* @return 解密后的明文
*/
public static byte[] decrypt(PrivateKey privateKey, byte[] encryptPassword) {
try {
Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, privateKey);
return cipher.doFinal(encryptPassword);
} catch (Exception e) {
throw new RuntimeException("RSA解密失败(RSA encrypt failed.)");
}
}
/**
* 密钥Base64
*
* @param privateKey 密钥
* @return 结果
*/
public static String getPrivateBase64(PrivateKey privateKey) {
return Base64.getEncoder().encodeToString(privateKey.getEncoded());
}
/**
* 公钥Base64
*
* @param publicKey 公钥
* @return 结果
*/
public static String getPublicBase64(PublicKey publicKey) {
return Base64.getEncoder().encodeToString(publicKey.getEncoded());
}
/**
* 根据公钥字符串获取公钥对象
*
* @param publicKeyString 公钥字符串
* @return 结果
*/
public static PublicKey getPublicKey(String publicKeyString)
throws NoSuchAlgorithmException, InvalidKeySpecException {
byte[] decode = Base64.getDecoder().decode(publicKeyString);
return KeyFactory.getInstance(RSA_ALGORITHM).generatePublic(new X509EncodedKeySpec(decode));
}
/**
* 根据密钥字符串获取密钥对象
*
* @param privateKeyString 密钥字符串
* @return 结果
*/
public static PrivateKey getPrivateKey(String privateKeyString)
throws NoSuchAlgorithmException, InvalidKeySpecException {
byte[] decode = Base64.getDecoder().decode(privateKeyString);
return KeyFactory.getInstance(RSA_ALGORITHM).generatePrivate(new PKCS8EncodedKeySpec(decode));
}
/**
* 对称加密AES 对称key生成
*
* @param passKey 关键密码
* @return 生成aes的key
* @throws NoSuchAlgorithmException 算法找不到异常
*/
public static SecretKey aesKey(String passKey) throws NoSuchAlgorithmException {
KeyGenerator keyGenerator = KeyGenerator.getInstance(AES_ALGORITHM);
SecureRandom secureRandom = new SecureRandom();
secureRandom.setSeed(passKey.getBytes());
keyGenerator.init(secureRandom);
return keyGenerator.generateKey();
}
/**
* @param mode 加解密模式:Cipher.ENCRYPT_MODE / Cipher.DECRYPT_MODE
* @param secretKey 对称key
* @param password 执行的密码
*/
public static byte[] aes(int mode, SecretKey secretKey, byte[] password) {
try {
Cipher instance = Cipher.getInstance(AES_ALGORITHM);
instance.init(mode, secretKey);
return instance.doFinal(password);
} catch (Exception e) {
throw new RuntimeException(String.format("AES执行失败,Cipher.mode:%s(AES encrypt failed.)", mode));
}
}
public static void main(String[] args) throws Exception {
String passkey = "dd";
KeyPair dd = genratorKeyPair(passkey);
String pu = new String(Base64.getEncoder().encode(dd.getPublic().getEncoded()));
String en = new String(Base64.getEncoder().encode(dd.getPrivate().getEncoded()));
System.out.println("publicKey:\n" + pu);
System.out.println("private:\n" + en);
// 加解密方案1:RSA + AES双重加密
// 1. AES加密
SecretKey key = aesKey(passkey);
byte[] aesEn = aes(Cipher.ENCRYPT_MODE, key, "123456".getBytes());
// 2. 通过RSA公钥加密密码
byte[] rsaEn = encrypt(dd.getPublic(), aesEn);
// 3. 通过RSA私钥解密密码
byte[] rsaDe = decrypt(dd.getPrivate(), rsaEn);
// 4. 再同AES解密
byte[] aesDe = aes(Cipher.DECRYPT_MODE, key, rsaDe);
System.out.println("两重解密:" + new String(aesDe));
// 加解密方案2:RSA加密
byte[] encrypt = encrypt(RsaUtil.getPublicKey(pu), "123456".getBytes());
byte[] decrypt = decrypt(RsaUtil.getPrivateKey(en), encrypt);
System.out.println("RSA解密:" + new String(decrypt));
}
}
generar par de claves
Ejecute RsaUtil
el método principal, genere publicKey
y privateKey
, y luego privateKey
guárdelo en el backend y publicKey
déselo al frontend, y luego, cuando el frontend pasa la contraseña al backend, primero pasa RSA
y publicKey
encripta;
Ponga la clave privada en el archivo: privateKey.pem
, use pem
o der
sufije el almacenamiento de archivos de formato, de lo contrario habrá problemas en el análisis
leer clave privada
SecurityWebConfig
Agregue un constructor para leer la clave privada durante la inicialización (este método se puede usar para empaquetar y se puede usar con confianza);
privateKey se establece en público y se puede llamar en cualquier parte del proyecto.
public static PrivateKey privateKey;
public SecurityWebConfig() throws Exception {
try(InputStream is = this.getClass().getClassLoader().getResourceAsStream("privateKey.pem")) {
if (is == null) {
throw new RuntimeException("没有读取的密钥!!!");
}
byte[] data = new byte[2048];
int length = is.read(data);
String privateKeyString = new String(data, 0, length);
privateKey = RsaUtil.getPrivateKey(privateKeyString.trim());
}
}
descifrado de filtros
CustomUsernamePasswordAuthenticationFilter
Descifre la contraseña en la personalizada y luego authenticationToken
configúrela, porque el último método de verificación matches
debe hacer coincidir la contraseña antes del cifrado y la contraseña después del cifrado;
@Slf4j
public class CustomUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private static String decondePassword(String password) throws NoSuchAlgorithmException, InvalidKeySpecException {
byte[] decode = Base64.getDecoder().decode(password.getBytes());
return new String(RsaUtil.decrypt(SecurityWebConfig.privateKey, decode));
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
try (InputStream inputStream = request.getInputStream()) {
ObjectMapper objectMapper = new ObjectMapper();
Map<String, String> loginRequest = objectMapper.readValue(inputStream, Map.class);
String username = loginRequest.get(super.getUsernameParameter());
String password = loginRequest.get(super.getPasswordParameter());
try {
password = decondePassword(password);
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
log.error("密码解密失败!!!!", e);
password = null;
}
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
// 使用 AuthenticationManager 进行身份验证
return this.getAuthenticationManager().authenticate(authenticationToken);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
Por supuesto, el proceso de descifrado también se puede descifrar en el lugar que realmente coincide, y el org.springframework.security.authentication.dao.DaoAuthenticationProvider#additionalAuthenticationChecks
método de reescritura tiene la misma lógica, por lo que no es necesario escribir código.
Agregar filtro personalizado a la seguridad
@Override
protected void configure(HttpSecurity http) throws Exception {
// 表单
// 注意:使用了`BasicAuthenticationFilter`那么在配置`WebSecurityConfigurerAdapter`时,就不要设置`http.httpBasic()`,不然不会走我们自定义的Filter
http
.formLogin()
// 登录的请求接口,对应表单的action,这里只是修改了处理的api,实际处理还是security
.loginProcessingUrl("/loginDeal")
// 登录的用户名和密码参数名称
.usernameParameter("username")
.passwordParameter("pwd")
.successHandler(new LoginSuccessHandler())
.failureHandler(new LoginFailureHandler())
.permitAll()
// 开启认证
.and().authorizeRequests()
//设置哪些路径可以直接访问,不需要认证
.antMatchers("/test/*").permitAll()
//需要认证
.anyRequest().authenticated()
.and().csrf().disable(); //关闭csrf防护
// 将我们的repository设置到共享变量里
http.setSharedObject(SecurityContextRepository.class, securityContextRepository);
// 设置序列化
http.exceptionHandling().authenticationEntryPoint(new CustomizeAuthenticationEntryPoint());
// 自定义过滤器加入security
http.addFilterBefore(customUsernamePasswordAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}
Configuración de front-end e inicio de sesión
Aquí use el proyecto de plantilla (), después de bajar, instale la herramienta de cifrado
npm instalar jsencrypt
instalar npm
-
Modifique
axios.js
el finalAxios.interceptors.response.
a lo siguiente:Axios.interceptors.response.use((res) => { if (res.config.direct) { return res.data } return Promise.resolve(res) }, (error) => { Message.error(error.response.data) return Promise.reject(error.response.data) })
-
Añadir método de inicio de sesión
export function login (data, success, error) { http.post('/loginDeal', data, success, error) }
-
Configurar la clave en una variable de entorno
Aquí solo se requiere configuración
dev.env.js
in yprod.env.js
in, dev es el entorno de desarrollo y prod es el entorno de producción.Nota: el valor de la variable aquí está marcado con una comilla doble y una comilla simple, de modo que cuando se use, no se reemplazará
webpack
directamente en el momento de la compilación y causará un error de sintaxis.module.exports = merge(prodEnv, { NODE_ENV: '"development"', PUBLIC_KEY: '"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnQRntjLw0wC4xPki/Tvf+7esQwf2PoCmvb8oKypvssevr8LK74CF/Yh0AjMvmoAlr0UXm5VK4B2edmvLwLTeFjAU8zXdNLlhC7YDUpc/vEZFPhh2jvUMjOe0LAJb+FOv5oMGpAxuj8PC9Cz4L05T/gOI7w8FPwCJjXJacWPhhSAK+dViXHLZVqNeIo4YRUT8C2s5e+vz03FByd511YaydVTbBGRB7+QVFJ5f6Rt9buxn9gDK5CcZ27ScQvdc88w9NF0bfmNRh8xec3Cz9uMyRVhy5d3pJM9a6jTEHcbOTapUAjssq2cVr+qx5DGv87u4I8qKqJQIhvu40Vd3foR0JQIDAQAB"' })
-
Crear
login.vue
(simplemente lo configuré aquí)Para el formulario de inicio de sesión, separo el campo de contraseña ingresado por el usuario del campo de contraseña transmitido al backend, para mejorar la experiencia del usuario.
<template> <div> <el-row> <el-col :span="6"> <el-form :model="form" status-icon :rules="rules" ref="ruleForm" label-width="100px" class="demo-ruleForm"> <el-form-item label="用户名" prop="username"> <el-input type="text" v-model="form.username"></el-input> </el-form-item> <el-form-item label="密码" prop="password"> <el-input type="password" v-model="form.password"></el-input> </el-form-item> <el-form-item> <el-button type="primary" @click="submitForm('ruleForm')">提交</el-button> </el-form-item> </el-form> </el-col> </el-row> </div> </template> <script> import {loginApi} from '../api' import {JSEncrypt} from 'jsencrypt' export default { name: 'login', data () { return { form: { username: '', password: '', pwd: '' }, rules: { password: [ {required: true, trigger: 'blur', message: '请输入密码'} ], username: [ {required: true, trigger: 'blur', message: '请输入用户名'} ] } } }, methods: { submitForm (formName) { this.$refs[formName].validate((valid) => { if (valid) { // 加密密码 this.form.pwd = this.encryptedData(process.env.PUBLIC_KEY, this.form.password) this.form.password = null loginApi.login(this.form, success => { this.$message.success('登录成功') // 存储token信息,和用户信息 // 跳转指定页面 console.log(success) }) } else { this.$message.info('表单验证失败') return false } }) }, // 加密密码 encryptedData (publicKey, data) { // 新建JSEncrypt对象 let encryptor = new JSEncrypt() // 设置公钥 encryptor.setPublicKey(publicKey) // 加密数据 return encryptor.encrypt(data) } } } </script> <style scoped> </style>
-
route/index.js
Agregar enrutamiento{ path: '/login', name: 'login', component: login }
-
npm run dev
-
navegador
http://localhost:8080/#/login
resultado
Inicio de sesión de Captcha (autorización personalizada-captcha)
Continúe para completar el ejemplo anterior de cifrado y descifrado.
El principio es que el backend genera aleatoriamente una serie de números, los guarda y luego genera una imagen para el frontend, o aleatoriza una fórmula de cálculo simple, como: luego calcula el valor, lo guarda y luego genera una imagen 1+8=
para la interfaz con esta fórmula. El foco está en el antes y el después. Cómo conectar, lo uso aquí sessionId
.
Tenga en cuenta que el código de verificación no se puede usar todo el tiempo, y la aleatoriedad del código de verificación debe proporcionarse a través del tiempo y las estrategias de verificación.
generar código de verificación
Las herramientas utilizadas por la herramienta captcha aquí Hutool
,
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-captcha</artifactId>
<version>5.8.15</version>
</dependency>
Aquí inyecto la herramienta generada por el algoritmo como singleton, y luego la uso durante la verificación, y luego el código de verificación caduca en 2 minutos
/**
* 认证
*
* @author ALI
* @since 2023/6/10
*/
@Service
public class AuthServiceImpl implements AuthService {
@Autowired
private CacheManager cacheManager;
@Autowired
private MathGenerator mathGenerator;
@Override
public void captcha(HttpServletRequest request, HttpServletResponse response) {
ShearCaptcha captcha = CaptchaUtil.createShearCaptcha(200, 45, 4, 4);
// 自定义验证码内容为四则运算方式
captcha.setGenerator(mathGenerator);
// 重新生成code
captcha.createCode();
String code = captcha.getCode();
String id = request.getSession().getId();
cacheManager.set(AuthConstant.buildCaptchaKey(id), code, 2L, TimeUnit.MINUTES);
try (ServletOutputStream os = response.getOutputStream()) {
// 这里通过base64加密再返回给前端,前端就不用处理了
os.write(Base64.getEncoder().encode(captcha.getImageBytes()));
} catch (IOException e) {
throw new RuntimeException("验证码异常!");
}
}
}
No utilicé la herramienta hutool directamente aquí captcha.write(os);
, porque si hago esto, escribirá el flujo de bytes directamente en el front-end, y luego el front-end necesita procesarlo nuevamente, así que aquí obtengo el byte, lo encripto nuevamente , y Base64
luego Escrito para el front-end, de modo que el front-end solo necesita deletrear uno antes del resultado data:image/png;base64,
para usarlo.
Proporcionar interfaz de código de verificación
@GetMapping("/login/captcha")
public String captcha(HttpServletRequest request, HttpServletResponse response) {
authService.captcha(request, response);
return null;
}
Crear filtro captcha
Personalice un filtro y agréguelo a la cadena de intercepción de seguridad.
El filtro de código de verificación personalizado aquí, heredado OncePerRequestFilter
, solo irá una vez, por supuesto, solo necesita ir una vez y eliminar la clave después de que la verificación sea exitosa;
/**
* 自定义的验证码过滤器
*
* @author ALI
* @since 2023/6/10
*/
public class CustomCaptchaFilter extends OncePerRequestFilter {
public String loginApi;
private CacheManager cacheManager;
private MathGenerator mathGenerator;
private AuthenticationEntryPoint entryPoint;
public CustomCaptchaFilter(CacheManager cacheManager, MathGenerator mathGenerator, AuthenticationEntryPoint authenticationEntryPoint,
String loginApi) {
this.cacheManager = cacheManager;
this.mathGenerator = mathGenerator;
this.entryPoint = authenticationEntryPoint;
this.loginApi = loginApi;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
if (request.getRequestURI().equals(loginApi)) {
String id = request.getSession().getId();
String key = AuthConstant.buildCaptchaKey(id);
Object match = cacheManager.get(key);
if (match == null) {
entryPoint.commence(request, response, new BadCredentialsException("验证码过期!"));
return;
}
// 注意这里的验证码参数不是从body里获取的
if (!mathGenerator.verify(match.toString(), request.getParameter(AuthConstant.CAPTCHA))) {
entryPoint.commence(request, response, new BadCredentialsException("验证码验证错误!"));
return;
}
// 验证成功删除
cacheManager.delete(key);
}
filterChain.doFilter(request, response);
}
}
Agregar filtro a la seguridad
@Configuration
@EnableWebSecurity
public class SecurityWebConfig extends WebSecurityConfigurerAdapter {
private static final String loginApi = "/loginDeal";
private static final String usernameParameter = "username";
private static final String passwordParameter = "pwd";
public static PrivateKey privateKey;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private CustomSecurityContextRepository securityContextRepository;
@Autowired
private CacheManager cacheManager;
public SecurityWebConfig() throws Exception {
try (InputStream is = this.getClass().getClassLoader().getResourceAsStream("privateKey.pem")) {
if (is == null) {
throw new RuntimeException("没有读取的密钥!!!");
}
byte[] data = new byte[2048];
int length = is.read(data);
String privateKeyString = new String(data, 0, length);
privateKey = RsaUtil.getPrivateKey(privateKeyString.trim());
}
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public CorsFilter corsFilter() {
return new CorsFilter(corsConfigurationSource());
}
// 注入算法生成工具
@Bean
public MathGenerator mathGenerator() {
return new MathGenerator(1);
}
/**
* 自定义的customUsernamePasswordAuthenticationFilter
* 需要同步在HttpSecurity里的配置
*/
public CustomUsernamePasswordAuthenticationFilter customUsernamePasswordAuthenticationFilter() throws Exception {
CustomUsernamePasswordAuthenticationFilter result = new CustomUsernamePasswordAuthenticationFilter();
result.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher(loginApi));
result.setAuthenticationManager(this.authenticationManager());
result.setUsernameParameter(usernameParameter);
result.setPasswordParameter(passwordParameter);
result.setAuthenticationSuccessHandler(new LoginSuccessHandler());
result.setAuthenticationFailureHandler(new LoginFailureHandler());
return result;
}
/**
* 跨域设置
*/
private CorsConfigurationSource corsConfigurationSource() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
// 允许cookies跨域
config.setAllowCredentials(true);
// 允许向该服务器提交请求的URI,* 表示全部允许,在SpringMVC中,如果设成*,会自动转成当前请求头中的Origin
config.addAllowedOrigin("*");
// 允许访问的头信息,* 表示全部
config.addAllowedHeader("*");
// 预检请求的缓存时间(秒),即在这个时间段里,对于相同的跨域请求不会再预检了
config.setMaxAge(18000L);
// 允许提交请求的方法,* 表示全部允许
config.addAllowedMethod("GET");
config.addAllowedMethod("PUT");
config.addAllowedMethod("POST");
config.addAllowedMethod("DELETE");
source.registerCorsConfiguration("/**", config);
return source;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("ali")
.password(passwordEncoder().encode("123456"))
.roles("admin");
auth.userDetailsService(userDetailsService);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 表单
// 注意:使用了`BasicAuthenticationFilter`那么在配置`WebSecurityConfigurerAdapter`时,就不要设置`http.httpBasic()`,不然不会走我们自定义的Filter
http
.formLogin()
// 自定义登录页
.loginPage("/login.html")
// 登录的请求接口,对应表单的action,这里只是修改了处理的api,实际处理还是security
.loginProcessingUrl("/loginDeal")
// 不能写:successForwardUrl("/index.html"),会报405
.defaultSuccessUrl("/index.html")
// 登录失败转发到哪个页面
.failureForwardUrl("/login.html?error=true")
// 登录的用户名和密码参数名称
.usernameParameter("username")
.passwordParameter("pwd")
.successHandler(new LoginSuccessHandler())
.failureHandler(new LoginFailureHandler())
.permitAll()
// 开启认证
.and().authorizeRequests()
//设置哪些路径可以直接访问,不需要认证
.antMatchers("/test/*", "/login/**").permitAll()
//需要认证
.anyRequest().authenticated()
.and().csrf().disable(); //关闭csrf防护
// 将我们的repository设置到共享变量里
http.setSharedObject(SecurityContextRepository.class, securityContextRepository);
// 设置序列化
http.exceptionHandling().authenticationEntryPoint(new CustomizeAuthenticationEntryPoint());
http.addFilterBefore(customUsernamePasswordAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
// 添加验证码过滤器
http.addFilterBefore(new CustomCaptchaFilter(cacheManager, mathGenerator(), entryPoint, loginApi), WebAsyncManagerIntegrationFilter.class);
}
}
Agregué el filtro del código de verificación aquí al WebAsyncManagerIntegrationFilter
frente. Es el primer filtro. La parte de verificación del código de verificación es la primera, lo cual es muy razonable. Al mismo tiempo, al CustomizeAuthenticationEntryPoint
escribir directamente, ya no seguirá la lógica de seguridad.
Código de verificación de la pantalla frontal (login2.vue)
<template>
<div>
<el-row>
<el-col :span="6">
<el-form :model="form" status-icon :rules="rules" ref="ruleForm" label-width="100px" class="demo-ruleForm">
<el-form-item label="用户名" prop="username">
<el-input type="text" v-model="form.username"></el-input>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input type="password" v-model="form.password"></el-input>
</el-form-item>
<el-form-item label="验证码" prop="captcha">
<el-input type="text" v-model="form.captcha"></el-input>
<el-row>
<el-col :span="12" ><img :src="captchaImage" alt="0"/></el-col>
<el-col :span="12" ><a href="javascript:void(0)" @click="renewCaptcha">看不清,换一张</a></el-col>
</el-row>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm('ruleForm')">提交</el-button>
</el-form-item>
</el-form>
</el-col>
</el-row>
</div>
</template>
<script>
import {
loginApi} from '../api'
import {
JSEncrypt} from 'jsencrypt'
export default {
name: 'login2',
data () {
return {
form: {
username: '',
password: '',
pwd: '',
captcha: ''
},
captchaImage: null,
rules: {
password: [
{
required: true, trigger: 'blur', message: '请输入密码'}
],
username: [
{
required: true, trigger: 'blur', message: '请输入用户名'}
],
captcha: [
{
required: true, trigger: 'blur', message: '请输入验证码'}
]
}
}
},
mounted () {
this.getCaptchaPic()
},
methods: {
submitForm (formName) {
this.$refs[formName].validate((valid) => {
if (valid) {
// 加密密码
this.form.pwd = this.encryptedData(process.env.PUBLIC_KEY.toString(), this.form.password)
this.form.password = null
loginApi.login(this.form, success => {
this.$message.success('登录成功')
// 存储token信息,和用户信息
// 跳转指定页面
console.log(success)
}, error => {
// 验证码过期就刷新验证码
if (error.message.indexOf('过期') > 0) {
this.getCaptchaPic()
}
})
} else {
this.$message.info('表单验证失败')
return false
}
})
},
// 加密密码
encryptedData (publicKey, data) {
// 新建JSEncrypt对象
let encryptor = new JSEncrypt()
// 设置公钥
encryptor.setPublicKey(publicKey)
// 加密数据
return encryptor.encrypt(data)
},
getCaptchaPic () {
loginApi.captcha(null, success => {
this.captchaImage = 'data:image/png;base64,' + success.data
})
},
// 验证码刷新
renewCaptcha () {
this.getCaptchaPic()
}
}
}
</script>
<style scoped>
</style>
Coloque el código de verificación en la URL aquí, para que el backend pueda obtener el código de verificación directamente
export function login (data, success, error) {
http.post('/loginDeal?captcha=' + data.captcha, data, success, error)
}
resultado
Código de verificación de interfaz
En este caso, debe verificar el código de verificación al iniciar sesión.
extremo posterior
@GetMapping("/login/captcha/valid")
public Boolean validCaptcha(HttpServletRequest request) {
return authService.validCaptcha(request);
}
Lo mismo aquí, elimine el código de verificación después de que la verificación sea exitosa;
@Override
public boolean validCaptcha(HttpServletRequest request) {
String id = request.getSession().getId();
String key = AuthConstant.buildCaptchaKey(id);
Object match = cacheManager.get(key);
if (match == null) {
throw new RuntimeException("验证码过期!");
}
// 注意这里的验证码参数不是从body里获取的
boolean captcha = mathGenerator.verify(match.toString(), request.getParameter("captcha"));
if (captcha) {
cacheManager.delete(key);
}
return captcha;
}
Interfaz (login3.vue)
export function validCaptcha (data, success, error) {
http.get('/login/captcha/valid?captcha=' + data, null, success, error)
}
Para iniciar sesión aquí, primero verifique el código de verificación y luego llame al inicio de sesión después de tener éxito. Hay miles de métodos, y estoy dando un ejemplo aquí.
submitForm (formName) {
this.$refs[formName].validate((valid) => {
if (valid) {
// 加密密码
this.form.pwd = this.encryptedData(process.env.PUBLIC_KEY.toString(), this.form.password)
this.form.password = null
this.validCaptchaAndLogin(this.form.captcha)
} else {
this.$message.info('表单验证失败')
return false
}
})
},
validCaptchaAndLogin (captcha) {
loginApi.validCaptcha(captcha, success => {
loginApi.login(this.form, success => {
this.$message.success('登录成功')
// 存储token信息,和用户信息
// 跳转指定页面
console.log(success)
}, error => {
// 验证码过期就刷新验证码
if (error.message.indexOf('过期') > 0) {
this.getCaptchaPic()
}
})
}, error => {
console.log(error)
this.getCaptchaPic()
})
}
Autorización/Control de acceso (custom-auth-control)
Serialización de respuestas para permisos
Del mismo modo CustomizeAuthenticationEntryPoint
, se requiere la serialización de la configuración.
/**
* 访问拒绝处理器
* @author ALI
* @since 2023/6/11
*/
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
private AuthenticationEntryPoint entryPoint;
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException)
throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
ResponseData<Object> result = ResponseData.failed("无权限访问");
PrintWriter writer = response.getWriter();
writer.write(JSON.toJSONString(result));
writer.flush();
}
}
SecurityWebConfig
Cambio de configuracion
// 设置序列化
http.exceptionHandling().authenticationEntryPoint(new CustomizeAuthenticationEntryPoint())
.accessDeniedHandler(new CustomAccessDeniedHandler());
Configuración de permisos
Al guardar la información del rol en nuestra tabla en seguridad, necesitamos hacer algún procesamiento. El siguiente es el constructor para personalizar la información del usuario. Aquí, se agrega un prefijo al código del rol del usuario. Este es el método predeterminado de el marco de seguridad ROLE_
.
public CustomUser(SysUser sysUser) {
this.userId = sysUser.getId();
this.username = sysUser.getUsername();
this.password = sysUser.getPassword();
if (!CollectionUtils.isEmpty(sysUser.getRoles())) {
this.authorities = sysUser.getRoles().stream().map(d -> new SimpleGrantedAuthority("ROLE_" + d)).collect(Collectors.toList());
}
}
Habilitar la interceptación de anotaciones
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class SecurityAuthConfig {
}
autoridad de ruta
WebSecurityConfigurerAdapter
Esto se logra a través de la herencia void configure(HttpSecurity http) throws Exception
;
@Override
protected void configure(HttpSecurity http) throws Exception {
// 表单
// 注意:使用了`BasicAuthenticationFilter`那么在配置`WebSecurityConfigurerAdapter`时,就不要设置`http.httpBasic()`,不然不会走我们自定义的Filter
http
.formLogin()
// 登录的请求接口,对应表单的action,这里只是修改了处理的api,实际处理还是security
.loginProcessingUrl("/loginDeal")
// 登录的用户名和密码参数名称
.usernameParameter("username")
.passwordParameter("pwd")
.successHandler(new LoginSuccessHandler())
.failureHandler(new LoginFailureHandler())
.permitAll()
.and().csrf().disable(); //关闭csrf防护
// 将我们的repository设置到共享变量里
http.setSharedObject(SecurityContextRepository.class, securityContextRepository);
// 设置序列化
http.exceptionHandling().authenticationEntryPoint(new CustomizeAuthenticationEntryPoint())
.accessDeniedHandler(new CustomAccessDeniedHandler());
http.addFilterBefore(customUsernamePasswordAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
http.authorizeRequests()
// 路径/test /login多级的都可以放行
.antMatchers("/test/*", "/login/**").permitAll()
// 访问getData2 需要角色dev
.antMatchers("/getData2").hasRole("dev")
// 访问getData3 需要角色 admin
.antMatchers("/getData3").hasRole("admin")
// 访问getData4 需要角色dev test1
.antMatchers("/getData4").hasAnyRole("test1","dev")
// 访问getData5 需要角色test1
.antMatchers("/getData5").hasAuthority("ROLE_test1")
// 访问getData6 需要为127.0.0.1
.antMatchers("/getData6").hasIpAddress("127.0.0.1")
// 访问getData7 需要角色test2 test3
.antMatchers("/getData7").hasAnyAuthority("ROLE_test2", "ROLE_test3")
.anyRequest().authenticated();
}
En seguridad, el rol tendrá un prefijo predeterminado , por lo que se debe omitir el prefijo ROLE_
al usarlo y se agregará al juzgar, pero no se agregará cuando se incluya .hasRole,hasAnyRole
ROLE_
Authority
ROLE_
permisos de método
Controle los permisos a través de anotaciones, admita anotaciones en las clases y tenga las últimas anotaciones al mismo tiempo.
5 anotaciones de permisos proporcionadas por Spring:
- @preautorizar
- @PostAutorizar
- @prefiltro
- @postfiltro
- @Asegurado
Anotaciones del protocolo JSR-250:
- @rolespermitidos
- @PermitAll
- @DenyAll
Estas anotaciones deben poder utilizar@EnableGlobalMethodSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
prePostEnabled = true
: Abrir @PreAuthorize,@PostAuthorize,@PreFilter ,@PostFilter
4 anotaciones
securedEnabled = true
: @Secured
anotación abierta
jsr250Enabled = true
: @RolesAllowed,@PermitAll,@DenyAll
anotación abierta
anotación de permiso de primavera
Las anotaciones deben estar habilitadas@EnableGlobalMethodSecurity(prePostEnabled = true)
@preautorizar
Generalmente, se utiliza esta anotación, y esta anotación es relativamente simple y true
tiene derechos de acceso cuando el valor es;
// 这种方式匹配的是ROLE_dev,内部会默认添加ROLE_
@PreAuthorize("hasRole('dev')")
@GetMapping("/method1")
public String method1() {
return "method1";
}
// 匹配ROLE_dev
@PreAuthorize("hasAuthority('ROLE_dev')")
@GetMapping("/method2")
public String method2() {
return "method2";
}
// 只能由用户名带后缀 _ad 的访问
@PreAuthorize("principal.username.endsWith('_ad')")
@GetMapping("/method4")
public String method4() {
return "method4";
}
// 只能由用户名和参数相等
@PreAuthorize("principal.username.equals(#name)")
@GetMapping("/method44")
public String method44(String name) {
return "method44";
}
// 新增的用户,用户名只能是 _ad 结尾
@PreAuthorize("#user.username.endsWith('_ad')")
@PostMapping("/method5")
public String method5(@RequestBody SysUser user) {
return "method5";
}
// 是否是 admin 的角色,无关大小写
// principal 为内置对象
@PreAuthorize("principal?.isAdmin()")
@GetMapping("/method7")
public String method7() {
return "method7";
}
Además hay una herencia de roles.
@Bean
static RoleHierarchy roleHierarchy() {
return new RoleHierarchyImpl("ROLE_admin > ROLE_dev");
}
// 配置了 ROLE_admin > ROLE_dev
// dev 继承了admin 的权限,使用admin的用户访问
@PreAuthorize("hasRole('dev')")
@GetMapping("/method6")
public String method6() {
return "method6";
}
@PostAutorizar
La función de esta anotación es verificar el permiso después de que se ejecuta el método, y cuando el valor true
es el permiso de acceso;
// 返回的用户id必须 =10
// principal 为内置对象
@PostAuthorize("returnObject.id.equals('10')")
@GetMapping("/method8")
public SysUser method8() {
SysUser result = new SysUser();
result.setId("10");
return result;
}
@prefiltro
@PreFilter
Los parámetros del tipo de colección se pueden filtrar y false
los elementos se eliminan cuando se establece el valor.
// 过滤出 id= 1 的元素
// filterObject 内置对象,表示集合中的每一个元素
// userList 集合对象
@PreFilter(value = "filterObject.id.equals('1')", filterTarget = "userList")
@PostMapping("/method9")
public List<SysUser> method9(@RequestBody List<SysUser> userList) {
return userList;
}
@postfiltro
@PostFilter
La respuesta del tipo de colección se puede filtrar para false
eliminar elementos cuando el valor es .
// 过滤出 id != 1 的元素
@PostFilter(value = "!filterObject.id.equals('1')")
@PostMapping("/method10")
public List<SysUser> method10(@RequestBody List<SysUser> userList) {
return userList;
}
@Asegurado
Esta anotación se usa especialmente para determinar si el usuario tiene el rol, el valor es el nombre del rol, recuerda agregar el prefijoROLE_
// 只允许角色为 ROLE_admin 的用户访问
@Secured("ROLE_admin")
@PostMapping("/method11")
public List<SysUser> method11(@RequestBody List<SysUser> userList) {
return userList;
}
Anotaciones JSR-250
@RolesAlowe
El valor es una matriz de nombres de roles, recuerde agregarROLE_
// 允许角色为 ROLE_admin ROLE_dev 的用户访问
@RolesAllowed({
"ROLE_admin", "ROLE_dev"})
@PostMapping("/method12")
public List<SysUser> method12(@RequestBody List<SysUser> userList) {
return userList;
}
@PermitAll
Publicación directa, sin verificación de permisos, se puede @RolesAllowed
combinar y no se puede usar con anotaciones de permisos definidas por Spring;
@RolesAllowed("ROLE_test1")
@RestController
@RequestMapping("/method2")
public class MethodController2 {
@PermitAll
@GetMapping("/get1")
public String method1() {
return "get1";
}
}
@DenyAll
Por el contrario @PermitAll
, nadie puede acceder
@DenyAll
@GetMapping("/get2")
public String method2() {
return "get2";
}
permisos dinámicos
principio:
La implementación de permisos se puede refinar a URL, es decir, permisos de menú. Por supuesto, los permisos a nivel de botón también pertenecen a los permisos de menú, y los permisos de menú se montan en roles. Por lo tanto, estos permisos de menú se pueden obtener a través de la sesión iniciada. información del usuario Luego, el control de permisos de acceso se puede realizar en el interceptor.
opcion uno
Crear lista de consultas de roles y permisos
@Data
public class RolePermission {
private Integer roleId;
private String roleName;
private String roleCode;
private Integer permissionId;
private Integer parentPermissionId;
private String permissionCode;
private String permissionName;
private String permissionUrl;
}
@Select("select a.id as role_id,a.name as role_name,a.code as role_code,b.name as permission_name,b.code as permission_code,b.id as permission_id , b.parent_id as parent_permission_id,b.url as permission_url "
+ "from sys_role a inner join sys_permission b on a.id = b.role_id where a.deleted = 0")
List<RolePermission> roleList();
Después de iniciar el proyecto, almacene roles y funciones en caché para facilitar el acceso;
Si se hace esto, el caché debe actualizarse al modificar los permisos de rol;
@Service
@AllArgsConstructor
public class RoleServiceImpl implements RoleService , InitializingBean {
private CacheManager cacheManager;
private RoleRepository roleRepository;
@Override
public void afterPropertiesSet() throws Exception {
List<RolePermission> roleList = roleRepository.roleList();
if (roleList.isEmpty()) {
return;
}
Map<String, List<RolePermission>> permissionMap = roleList.stream().collect(Collectors.groupingBy(RolePermission::getRoleCode));
for (Map.Entry<String, List<RolePermission>> entry : permissionMap.entrySet()) {
String key = AuthConstant.ROLE_PRE + entry.getKey();
List<String> collect = entry.getValue().stream().map(RolePermission::getPermissionUrl)
.filter(StringUtils::isNoneBlank).collect(Collectors.toList());
String value = JSON.toJSONString(collect);
cacheManager.set(key, value);
}
}
}
Cree un solo filtro PermissionFilter
para juzgar si la solicitud tiene permiso;
public class PermissionFilter extends OncePerRequestFilter {
private final CacheManager cacheManager;
public PermissionFilter(CacheManager cacheManager) {
this.cacheManager = cacheManager;
}
@Override
protected void doFilterInternal(HttpServletRequest servletRequest, HttpServletResponse servletResponse, FilterChain filterChain)
throws ServletException, IOException {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || authentication.getPrincipal() == null) {
filterChain.doFilter(servletRequest, servletResponse);
return;
}
if (authentication.getPrincipal() instanceof String) {
filterChain.doFilter(servletRequest, servletResponse);
return;
}
// 获取认证信息
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
if (authorities.isEmpty()) {
throw new AccessDeniedException("无权限!");
}
// 获取权限信息
Set<String> permissions = new HashSet<>();
for (GrantedAuthority authority : authorities) {
Object o = cacheManager.get(authority.getAuthority());
if (o != null) {
List<String> collect = JSONArray.parseArray(o.toString()).stream().map(Object::toString).collect(Collectors.toList());
permissions.addAll(collect);
}
}
if (permissions.isEmpty()) {
throw new AccessDeniedException("无权限!");
}
// 判断是否有权限访问
String api = servletRequest.getRequestURI();
if (!permissions.contains(api)) {
throw new AccessDeniedException("无权限!");
}
filterChain.doFilter(servletRequest, servletResponse);
}
}
Luego agregue el filtro de permiso al FilterSecurityInterceptor
frente, ExceptionTranslationFilter
detrás;
http.addFilterAfter(new PermissionFilter(cacheManager), FilterSecurityInterceptor.class);
Opción II
control de autenticación personalizado2
En esta solución, modifique la implementación del administrador de decisiones dentro de la seguridad, trate de no mover las cosas de seguridad;
primero personalice el administrador de decisiones de acceso
/**
* 自定义访问决策管理器
* @author ALI
* @since 2023/6/16
*/
public class CustomAccessDecisionManager implements AccessDecisionManager {
private final CacheManager cacheManager;
public CustomAccessDecisionManager(CacheManager cacheManager) {
this.cacheManager = cacheManager;
}
@Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes)
throws AccessDeniedException, InsufficientAuthenticationException {
// 获取认证信息
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
if (authorities.isEmpty()) {
throw new AccessDeniedException("无权限!");
}
// 获取权限信息
Set<String> permissions = new HashSet<>();
for (GrantedAuthority authority : authorities) {
Object o = cacheManager.get(authority.getAuthority());
if (o != null) {
List<String> collect = JSONArray.parseArray(o.toString()).stream().map(Object::toString).collect(Collectors.toList());
permissions.addAll(collect);
}
}
if (permissions.isEmpty()) {
throw new AccessDeniedException("无权限!");
}
FilterInvocation filterInvocation = (FilterInvocation) object;
// 判断是否有权限访问
String api = filterInvocation.getRequestUrl();
if (!permissions.contains(api)) {
throw new AccessDeniedException("无权限!");
}
}
@Override
public boolean supports(ConfigAttribute attribute) {
return true;
}
@Override
public boolean supports(Class<?> clazz) {
return true;
}
}
Modificar la configuración de seguridadSecurityWebConfig
@Override
public void init(WebSecurity web) throws Exception {
HttpSecurity http = getHttp();
web.addSecurityFilterChainBuilder(http).postBuildAction(() -> {
FilterSecurityInterceptor securityInterceptor = http.getSharedObject(FilterSecurityInterceptor.class);
securityInterceptor.setAccessDecisionManager(new CustomAccessDecisionManager(cacheManager));
web.securityInterceptor(securityInterceptor);
});
}
Hay otra forma de configurar aquí: el método de configuración recomendado oficial de Spring ( Java Configuration :: Spring Security ), que nos brinda la capacidad de modificar directamente las propiedades internas de los objetos. En comparación con el método anterior, esto puede modificar casi todos los objetos, incluidos :config,filter,handler,interceptor,provider,strategy,point,voter
http.anyRequest().authenticated()
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
public <O extends FilterSecurityInterceptor> O postProcess(O fsi) {
fsi.setPublishAuthorizationSuccess(true);
fsi.setAccessDecisionManager(new CustomAccessDecisionManager(cacheManager));
return fsi;
}
});
Configuración completa:
@Configuration
@EnableWebSecurity
public class SecurityWebConfig extends WebSecurityConfigurerAdapter {
private static final String loginApi = "/loginDeal";
private static final String usernameParameter = "username";
private static final String passwordParameter = "pwd";
public static PrivateKey privateKey;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private CustomSecurityContextRepository securityContextRepository;
@Autowired
private CacheManager cacheManager;
public SecurityWebConfig() throws Exception {
try (InputStream is = this.getClass().getClassLoader().getResourceAsStream("privateKey.pem")) {
if (is == null) {
throw new RuntimeException("没有读取的密钥!!!");
}
byte[] data = new byte[2048];
int length = is.read(data);
String privateKeyString = new String(data, 0, length);
privateKey = RsaUtil.getPrivateKey(privateKeyString.trim());
}
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public CorsFilter corsFilter() {
return new CorsFilter(corsConfigurationSource());
}
@Bean
public MathGenerator mathGenerator() {
return new MathGenerator(1);
}
/**
* 自定义的customUsernamePasswordAuthenticationFilter
* 需要同步在HttpSecurity里的配置
*/
public CustomUsernamePasswordAuthenticationFilter customUsernamePasswordAuthenticationFilter() throws Exception {
CustomUsernamePasswordAuthenticationFilter result = new CustomUsernamePasswordAuthenticationFilter();
result.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher(loginApi));
result.setAuthenticationManager(this.authenticationManager());
result.setUsernameParameter(usernameParameter);
result.setPasswordParameter(passwordParameter);
result.setAuthenticationSuccessHandler(new LoginSuccessHandler());
result.setAuthenticationFailureHandler(new LoginFailureHandler());
return result;
}
/**
* 跨域设置
*/
private CorsConfigurationSource corsConfigurationSource() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
// 允许cookies跨域
config.setAllowCredentials(true);
// 允许向该服务器提交请求的URI,* 表示全部允许,在SpringMVC中,如果设成*,会自动转成当前请求头中的Origin
config.addAllowedOrigin("*");
// 允许访问的头信息,* 表示全部
config.addAllowedHeader("*");
// 预检请求的缓存时间(秒),即在这个时间段里,对于相同的跨域请求不会再预检了
config.setMaxAge(18000L);
// 允许提交请求的方法,* 表示全部允许
config.addAllowedMethod("GET");
config.addAllowedMethod("PUT");
config.addAllowedMethod("POST");
config.addAllowedMethod("DELETE");
source.registerCorsConfiguration("/**", config);
return source;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("ali")
.password(passwordEncoder().encode("123456"))
.roles("admin");
auth.userDetailsService(userDetailsService);
}
@Override
public void init(WebSecurity web) throws Exception {
HttpSecurity http = getHttp();
web.addSecurityFilterChainBuilder(http).postBuildAction(() -> {
// 因为FilterSecurityInterceptor是在其他配置完成后执行的,所以只能在这里修改
// 详细看 org.springframework.security.config.annotation.web.builders.WebSecurity#performBuild
// 配置 CustomAccessDecisionManager 方式一
FilterSecurityInterceptor securityInterceptor = http.getSharedObject(FilterSecurityInterceptor.class);
securityInterceptor.setAccessDecisionManager(new CustomAccessDecisionManager(cacheManager));
web.securityInterceptor(securityInterceptor);
});
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 表单
// 注意:使用了`BasicAuthenticationFilter`那么在配置`WebSecurityConfigurerAdapter`时,就不要设置`http.httpBasic()`,不然不会走我们自定义的Filter
http
.formLogin()
// 登录的请求接口,对应表单的action,这里只是修改了处理的api,实际处理还是security
.loginProcessingUrl("/loginDeal")
// 登录的用户名和密码参数名称
.usernameParameter("username")
.passwordParameter("pwd")
.successHandler(new LoginSuccessHandler())
.failureHandler(new LoginFailureHandler())
.permitAll()
.and().csrf().disable(); //关闭csrf防护
// 将我们的repository设置到共享变量里
http.setSharedObject(SecurityContextRepository.class, securityContextRepository);
// 设置序列化
http.exceptionHandling().authenticationEntryPoint(new CustomizeAuthenticationEntryPoint())
.accessDeniedHandler(new CustomAccessDeniedHandler());
http.addFilterBefore(customUsernamePasswordAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
http.authorizeRequests()
.antMatchers("/test/*", "/login/**").permitAll()
.antMatchers("/getData2").hasRole("dev")
.antMatchers("/getData3").hasRole("admin")
.antMatchers("/getData4").hasAnyRole("test1")
.antMatchers("/getData5").hasAuthority("ROLE_test1")
.antMatchers("/getData6").hasIpAddress("127.0.0.1")
.antMatchers("/getData7").hasAnyAuthority("ROLE_test2", "ROLE_test3")
.anyRequest().authenticated()
// 配置 CustomAccessDecisionManager 方式二
// .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
// public <O extends FilterSecurityInterceptor> O postProcess(O fsi) {
// fsi.setPublishAuthorizationSuccess(true);
// fsi.setAccessDecisionManager(new CustomAccessDecisionManager(cacheManager));
// return fsi;
// }
// })
;
}
}
Dirección del almacén
https://gitee.com/LIRUIYI/test-security.git
https://gitee.com/LIRUIYI/test-security-web.git