Spring Security: 2 [Análisis de principios, gestión de sesiones, autenticación y autorización integradas en RBAC, JWT]

Directorio de artículos

3. Análisis de principios

3.1 Análisis estructural

El problema que resuelve Spring Security es el control de acceso de seguridad . La función de control de acceso de seguridad es en realidad interceptar todas las solicitudes que ingresan al sistema y verificar si cada solicitud puede acceder a los recursos que espera. Según el aprendizaje de conocimientos previos, se puede lograr a través de tecnologías como Filter o AOP. La protección de los recursos web de Spring Security se logra mediante Filter, así que comience con este Filter y profundice gradualmente en los principios de Spring Security.

Cuando se inicializa Spring Security, se creará un filtro de servlet llamado SpringSecurityFilterChain. El tipo es org.springframework.security.web.FilterChainProxy. Implementa javax.servlet.Filter, por lo que las solicitudes externas pasarán por esta clase. La siguiente figura es Spring Diagrama de estructura de la cadena de filtros de seguridad:

Insertar descripción de la imagen aquí

La implementación de las funciones de Spring Security se completa principalmente mediante una serie de cadenas de filtros que cooperan entre sí.

Insertar descripción de la imagen aquí

A continuación se presentan los filtros principales de la cadena de filtros y sus funciones:

SecurityContextPersistenceFilter Este filtro es la entrada y salida de todo el proceso de interceptación (es decir, el primer y último interceptor), obtendrá el SecurityContext del SecurityContextRepository configurado cuando se inicie la solicitud y luego lo establecerá en SecurityContextHolder. Una vez completada la solicitud, el SecurityContext retenido por SecurityContextHolder se guarda en el SecurityContextRepository configurado y el SecurityContext retenido por securityContextHolder se borra al mismo tiempo;

UsernamePasswordAuthenticationFilter se utiliza para manejar la autenticación de los envíos de formularios. El formulario debe proporcionar el nombre de usuario y la contraseña correspondientes. También contiene AuthenticationSuccessHandler y AuthenticationFailureHandler para su procesamiento después de un inicio de sesión exitoso o fallido. Estos se pueden cambiar según las necesidades;

FilterSecurityInterceptor se utiliza para proteger los recursos web y utiliza AccessDecisionManager para autorizar el acceso al usuario actual;

ExceptionTranslationFilter puede detectar todas las excepciones de FilterChain y manejarlas. Pero solo manejará dos tipos de excepciones: AuthenticationException y AccessDeniedException, y continuará lanzando otras excepciones.

3.1 Análisis del proceso de autenticación de inicio de sesión

Insertar descripción de la imagen aquí

Analicemos el proceso de certificación en detalle:

  1. El nombre de usuario y la contraseña enviados por el usuario se obtienen mediante el filtro UsernamePasswordAuthenticationFilter en SecurityFilterChain y se encapsulan como una solicitud de autenticación, generalmente la clase de implementación UsernamePasswordAuthenticationToken.

  2. Luego, el filtro envía la autenticación al administrador de autenticación (AuthenticationManager) para su autenticación.

  3. Después de una autenticación exitosa, el administrador de identidad AuthenticationManager devuelve una instancia de autenticación llena de información (incluida la información de permiso, información de identidad y los detalles mencionados anteriormente, pero la contraseña generalmente se elimina).

  4. El contenedor de contexto de seguridad SecurityContextHolder establece la autenticación llena de información en el paso 3 a través del método SecurityContextHolder.getContext().setAuthentication(...). Se puede ver que la interfaz AuthenticationManager (administrador de autenticación) es la interfaz central relacionada con la autenticación y el punto de partida para iniciar la autenticación, su clase de implementación es ProviderManager. Spring Security admite múltiples métodos de autenticación, por lo que ProviderManager mantiene una Lista para almacenar múltiples métodos de autenticación. Al final, AuthenticationProvider completa el trabajo de autenticación real. Sabemos que la clase de implementación AuthenticationProvider correspondiente del formulario web es DaoAuthenticationProvider, que mantiene internamente un UserDetailsService y es responsable de obtener UserDetails. Finalmente, AuthenticationProvider completa los detalles de usuario en la autenticación. La relación general entre los componentes centrales de la autenticación es la siguiente:

Diagrama simple de diagrama de flujo:

Insertar descripción de la imagen aquí

3.1.1 Servicio de detalles de usuario

En el proceso de análisis de ahora, vimos que DaoAuthenticationProvider llama a UserDetailsService para consultar datos y luego compararlos. La función de UserDetailsService en todo el proceso de autenticación solo es responsable de consultar datos. Ya sea que consultemos datos de la memoria o datos de la base de datos lo determinamos nosotros mismos. configuración A modo de comparación La operación la realiza DaoAuthenticationProvider internamente.

public interface UserDetailsService {
    
     

  	UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; 

}
3.1.2 Personalizar el servicio de detalles del usuario

Configuración original:

 @Bean
    public UserDetailsService userDetailsService() {
    
    
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(User.withUsername("zhangsan").password("123").authorities("p1").build());
        manager.createUser(User.withUsername("lisi").password("456").authorities("p2").build());
        return manager;
    }

En nuestra configuración anterior, consultamos datos en la memoria, pero en el desarrollo real del proyecto, consultamos la base de datos.

Operación personalizada de UserDetailsService

@Service
public class SpringDataUserDetailsService implements UserDetailsService {
    
    
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    
    
        //登录账号 
        System.out.println("username=" + username);
        // 根据账号去数据库查询... 
        // 这里暂时使用静态数据 
        UserDetails userDetails = User.withUsername(username).password("123").
                authorities("p1").build();
        return userDetails;
    }
}

Reinicie el proyecto, solicite autenticación y se llamará al método loadUserByUsername de SpringDataUserDetailsService para consultar la información del usuario.

3.1.3 Codificador de contraseña

Conozca ContraseñaCodificador:

Después de que el procesador de autenticación DaoAuthenticationProvider obtiene UserDetails a través de UserDetailsService, ¿cómo interactúa con la solicitud?

¿Qué tal comparar contraseñas en Autenticación?

Insertar descripción de la imagen aquí

Aquí, Spring Security ha hecho una abstracción para adaptarse a varios tipos de cifrado: DaoAuthenticationProvider compara contraseñas a través del método de coincidencias de la interfaz PasswordEncoder, y los detalles específicos de comparación de contraseñas dependen de la implementación:

public interface PasswordEncoder {
    
    
    String encode(CharSequence var1);

    boolean matches(CharSequence var1, String var2);

    default boolean upgradeEncoding(String encodedPassword) {
    
    
        return false;
    }
}

Spring Security proporciona muchos codificadores de contraseña integrados, que se pueden usar de inmediato. Para usar un determinado codificador de contraseña, solo necesita hacer lo siguiente:

Simplemente haga la siguiente declaración, de la siguiente manera:

@Bean 
public PasswordEncoder passwordEncoder() {
    
     
  return NoOpPasswordEncoder.getInstance(); 
} 

NoOpPasswordEncoder utiliza un método de coincidencia de cadenas y no realiza cifrado ni procesamiento de comparación de contraseñas.

En proyectos reales, se recomienda utilizar BCryptPasswordEncoder, Pbkdf2PasswordEncoder, SCryptPasswordEncoder, etc. Si está interesado, puede echar un vistazo a la implementación específica de estos PasswordEncoder.

Definido en la clase de configuración de seguridad:

@Bean 
public PasswordEncoder passwordEncoder() {
    
     
  return new BCryptPasswordEncoder(); 
} 

La prueba encontró : La contraseña codificada no se parece a BCrypt
Motivo : La contraseña en la base de datos está en texto claro. Después de cifrar y comparar la contraseña pasada desde la recepción, es inconsistente.

Utilice BCrypt para cifrar contraseñas

1. Confidencialidad y verificación de contraseñas

    @org.junit.Test
    public void test(){
    
    
        String gensalt = BCrypt.gensalt();
        System.out.println(gensalt);
        String password = BCrypt.hashpw("123",gensalt );
        System.out.println(password);

        boolean checkpw = BCrypt.checkpw("123", "$2a$10$XeDXzobQ32ExDoZ1XNh1DOvAxJFtZgwwM1njc.vOzeYRFHyYPv1ay");
        System.out.println(checkpw);

    }

2. Modifique el formato de la contraseña en la clase de configuración:

UserDetails userDetails = User.withUsername(username).password("$2a$10$m44lS0/w2yRIuFMzUIRJ9OFUq9HMaLm2eqkSlKdfASpyZJgYrGe2.").
                authorities("p1").build();

Nota: La contraseña almacenada en el proyecto real está en texto cifrado.

3.2 Análisis del proceso de autorización

3.2.1 Análisis de principios del método de configuración.

Diagrama de flujo :

A través de un inicio rápido , sabemos que Spring Security puede autorizar y proteger solicitudes web a través de http.authorizeRequests(). Spring Security utiliza un filtro estándar para establecer la interceptación de solicitudes web y, en última instancia, logra el acceso autorizado a los recursos.

Insertar descripción de la imagen aquí

Analizar el proceso de autorización:

Para interceptar solicitudes , los usuarios autenticados que accedan a recursos web protegidos serán interceptados por la subclase de FilterSecurityInterceptor en SecurityFilterChain.

Para obtener la política de acceso a recursos , FilterSecurityInterceptor obtendrá la colección de permisos necesaria para acceder al recurso actual de DefaultFilterInvocationSecurityMetadataSource, una subclase de SecurityMetadataSource.

SecurityMetadataSource es en realidad la abstracción de leer la política de acceso, y el contenido leído es en realidad la regla de acceso que configuramos. La lectura de la política de acceso es la siguiente:

http.authorizeRequests() .antMatchers("/r/r1").hasAuthority("p1") .antMatchers("/r/r2").hasAuthority("p2") ...

Finalmente, FilterSecurityInterceptor llamará a AccessDecisionManager para tomar decisiones de autorización, si la decisión se aprueba, se permitirá el acceso al recurso, de lo contrario se prohibirá el acceso.

Insertar descripción de la imagen aquí

3.2.2 Análisis de principios del método de anotación

La autorización basada en métodos se implementa mediante Aop.

Diagrama de análisis de procesos:

Insertar descripción de la imagen aquí

4. Gestión de sesiones

4.1 Obtener la identidad del usuario

Una vez autenticado el usuario, la información del usuario se puede guardar en la sesión para evitar la autenticación en cada operación del usuario. Spring Security proporciona administración de sesiones. Después de pasar la autenticación, la información de identidad se coloca en el contexto SecurityContextHolder. SecurityContext se vincula al hilo actual para facilitar la obtención de la identidad del usuario.

Cómo escribir:

@RequestMapping("/getUsername")
    public String getUsername(){
    
    
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        Object principal = authentication.getPrincipal();
        String username = "";
        if(principal instanceof UserDetails){
    
    
            username = ((UserDetails) principal).getUsername();
        }else{
    
    
            username=  principal.toString();
        }
        return username;
    } 

4.2 Control de sesión

Podemos controlar exactamente cuándo se crea una sesión y cómo interactúa Spring Security con ella a través de las siguientes opciones:

mecanismo describir
siempre Si no existe ninguna sesión, cree una
si es requerido Cree una sesión si es necesario (predeterminado) al iniciar sesión
nunca SpringSecurity no creará una sesión, pero si se crea una sesión en otra parte de la aplicación, Spring Security la usará.
apátrida SpringSecurity nunca creará una sesión ni utilizará una sesión

Configure esta opción a través de los siguientes métodos de configuración:

.and()            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

De forma predeterminada, Spring Security creará una nueva sesión para cada usuario que inicie sesión correctamente, que es ifRequired .

Si selecciona nunca , le indica a Spring Security que no cree una sesión para los usuarios que inician sesión correctamente. Sin embargo, si su aplicación crea una nueva sesión en algún lugar, Spring Security la usará.

Si usa stateless , significa que Spring Security no creará una sesión para los usuarios que inicien sesión correctamente y su aplicación no permitirá nuevas sesiones. E implicará que no se utilizan cookies, por lo que cada solicitud deberá volver a autenticarse. Esta arquitectura sin estado es adecuada para las API REST y sus mecanismos de autenticación sin estado.

5. Autenticación y autorización integradas en RBAC

5.1 Autenticación integrada

####5.1.1 Importar dependencias

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
</dependency>

Cuando comencé el proyecto, descubrí que había un problema entre dominios al acceder a la interfaz de usuario.

Motivo: porque el dominio cruzado enviará una solicitud previa para ver si el servidor admite el dominio cruzado, pero esta solicitud previa también será interceptada. Antes, juzgamos si era un controladorMétodo en el interceptor para decidir si liberarlo , pero ahora usamos SpringSecurity, que fue interceptado por SpringSecurity.

5.1.2 Configurar reglas
@Override
    protected void configure(HttpSecurity http) throws Exception {
    
    

        //进制 crsf
        http.csrf().disable();
        //配置拦截规则
        http.authorizeRequests().
                antMatchers("/api/code","/api/login","/api/logout").
                permitAll().
                anyRequest().
                authenticated();
    }

Reinicie el acceso: el código de verificación se ha publicado, pero cuando hace clic en iniciar sesión para llamar al inicio de sesión, sigue siendo el método de inicio de sesión que escribimos nosotros mismos antes. Queremos que SpringSecurity nos ayude a autenticar y anotar el código de inicio de sesión anterior en la clase de implementación LoginServiceImpl.

5.1.3 Personalizar UserDetailService
@Service
public class UserDetailServiceImpl implements UserDetailsService {
    
    
    @Autowired
    private EmployeeMapper employeeMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    
    
        //根据用户名去查询数据
        if(StringUtils.isEmpty(username)){
    
    
            return null;
        }
        Employee employee =  employeeMapper.selectByUsername(username);
        return User.withUsername(employee.getUsername()).password(employee.getPassword()).authorities("p1").build();
    }
}

Debido a que nuestros datos están almacenados en la base de datos, necesitamos personalizar UserDetailService para consultar los datos de la base de datos durante la operación, pero surge un nuevo problema: no se llamará a la clase que definimos.

Pensando: ¿Por qué no se llama a nuestro UserDetailService? Podría llamarse durante el aprendizaje anterior.

Motivo: El método de envío de formularios que usamos antes usaba directamente su filtro de procesamiento de formularios, pero ahora usamos el envío ajax en el front-end en lugar del envío de formularios. Su filtro de envío de expresiones no puede manejarlo y debemos manejarlo nosotros mismos.

5.1.4 Unirse al autenticador
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
    
    
    return super.authenticationManagerBean();
}

 @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    
    
        auth.userDetailsService(userDetailService).passwordEncoder(passwordEncoder());
    }

    @Bean
    public PasswordEncoder passwordEncoder(){
    
    
        return NoOpPasswordEncoder.getInstance();
    }
5.1.5 Llame al autenticador en loginService para la autenticación
     
        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(loginVO.getUsername(),loginVO.getPassword());
        Authentication authenticate =
                authenticationManager.authenticate(token);
        User user = (User) authenticate.getPrincipal();

Encontramos un nuevo problema aquí y descubrimos que se devuelve Usuario, pero necesitamos colocar el objeto Empleado en redis. El usuario solo contiene la contraseña de la cuenta y la información de permiso del usuario que ha iniciado sesión actualmente.

5.1.6 Usuario personalizado
@Getter
@Setter
public class  LoginUser implements UserDetails {
    
    
    private Employee employee;
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
    
    
        return null;
    }

    @Override
    public String getPassword() {
    
    
        return employee.getPassword();
    }

    @Override
    public String getUsername() {
    
    
        return employee.getUsername();
    }

    /**
     * 账户是否未过期,过期无法验证
     */
    @Override
    public boolean isAccountNonExpired() {
    
    
        return true;
    }

    /**
     * 指定用户是否解锁,锁定的用户无法进行身份验证
     *
     * @return
     */
    @Override
    public boolean isAccountNonLocked() {
    
    
        return true;
    }

    /**
     * 指示是否已过期的用户的凭据(密码),过期的凭据防止认证
     *
     * @return
     */
    @Override
    public boolean isCredentialsNonExpired() {
    
    
        return true;
    }

    /**
     * 是否可用 ,禁用的用户不能身份验证
     *
     * @return
     */
    @Override
    public boolean isEnabled() {
    
    
        return true;
    }
}

En UserDetailServiceImpl

@Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    
    
        //根据用户名去查询数据
        if(StringUtils.isEmpty(username)){
    
    
            return null;
        }
        Employee employee =  employeeMapper.selectByUsername(username);
        LoginUser loginUser = new LoginUser();
        loginUser.setEmployee(employee);
        return loginUser;
//        return User.withUsername(employee.getUsername()).password(employee.getPassword()).authorities("p1").build();
    }

loginServiceImpl

LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
Employee employee = loginUser.getEmployee();

Ahora puedo iniciar sesión, pero descubrí que el departamento de acceso administra estos recursos y ocurren los siguientes problemas: ¿Existe otro problema entre dominios? ¿No hemos resuelto ya el problema entre dominios?

Insertar descripción de la imagen aquí

Motivo: nuestras reglas de coincidencia, excepto "/api/code", "/api/login" y "/api/logout", deben interceptarse para determinar si se debe autenticar. En SpringSecurity, SecurityContextHolder.getContext().getAuthentication() Obtenga la información del usuario actual para ver si ha iniciado sesión.

5.1.8 Filtro personalizado
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    
    
    @Autowired
    private RedisUtils redisUtils;
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
    
    
        String userId = request.getHeader("userId");
       String objJson = redisUtils.get(Constant.LOGIN_EMPLOYEE + userId);
        if(!StringUtils.isEmpty(objJson)){
    
    
           
            Employee employee = JSON.parseObject(objJson, Employee.class);
            UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(employee.getUsername(),employee.getPassword());
            SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
        }
        filterChain.doFilter(request,response);

    }
}

Agregar configuración:

  http.addFilterBefore(authenticationTokenFilter,
                UsernamePasswordAuthenticationFilter.class);
  http.addFilterBefore(corsFilter,
                JwtAuthenticationTokenFilter.class);

5.2 Autorización integrada

5.2.1 Consultar información de autorización
public Collection<? extends GrantedAuthority> getAuthorities() {
    
    
    // 先查询出来当前用户是否是超级管理员
    PermissionMapper permissionMapper = SpringUtils.getBean(PermissionMapper.class);
    List<GrantedAuthority> list = new ArrayList<>();
    if(employee.isAdmin()){
    
    
        // 如果是分配所有权限
        List<Permission> permissions = permissionMapper.selectAll();
        // 如果不是分配用户所拥有的权限
        for (Permission permission : permissions) {
    
    
            list.add(new SimpleGrantedAuthority(permission.getExpression()));
        }
    }else{
    
    
        //根据用户id 查询用户所拥有权限结合
        List<String> expressions = permissionMapper.queryPermissionByEmpId(employee.getId());
        for (String expression : expressions) {
    
    
            list.add(new SimpleGrantedAuthority(expression));
        }
    }
    return list;
}
5.2.2 Agregar permisos para filtrar
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
    
    
    String userId = request.getHeader("userId");
    if(!StringUtils.isEmpty(userId)){
    
    

        String objJson = redisUtils.get(Constant.LOGIN_EMPLOYEE + userId);

        Employee employee = JSON.parseObject(objJson, Employee.class);
        LoginUser loginUser = new LoginUser();
        loginUser.setEmployee(employee);

        UsernamePasswordAuthenticationToken token =
                new UsernamePasswordAuthenticationToken(employee.getUsername(),employee.getPassword(),loginUser.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(token);
    }

    filterChain.doFilter(request,response);
}
5.2.3 Activar la compatibilidad con anotaciones

Publicar anotación en la clase de inicio: @EnableGlobalMethodSecurity(prePostEnabled = true)

@SpringBootApplication
@MapperScan(basePackages = "cn.wolfcode.mapper")
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class App {
    
    

    public static void main(String[] args) {
    
    
        SpringApplication.run(App.class,args);
    }
}

Anotación de publicación del método: @PreAuthorize("hasAuthority('role:queryByRoleId')")

5.2.4 Resolver el problema de no cargar permisos

Motivo: dado que el principio de nuestra interceptación de permisos de anotaciones es utilizar Aop, se mejorará el controlador y anotamos que el método no se puede obtener a través de la clase proxy.

resolver:

//3 从 Controller 中拿到所有的方法
Method[] methods = controller.getClass().getSuperclass().getDeclaredMethods();

6.JWT

6.1 Introducción a JWT

jsonwebtoken (JWT) es un estándar abierto (rfc7519) que define una forma compacta y autónoma de transmitir información de forma segura como un objeto JSON entre partes. Esta información se puede verificar y confiar porque está firmada digitalmente. jwt se puede firmar con un secreto (usando el algoritmo HMAC) o con un par de claves pública/privada usando RSA o ECDSA

En términos sencillos: JWT, abreviatura de JSON Web Token, se utiliza como token en aplicaciones web en forma de JSON y se utiliza para transmitir información de forma segura como un objeto JSON entre partes. Durante el proceso de transmisión de datos, también se pueden completar el cifrado de datos, la firma y otros procesos relacionados.

6.2 Qué puede hacer JWT

1. Autorización

Este es el escenario más común para usar JWT. Una vez que el usuario haya iniciado sesión, cada solicitud posterior incluirá el JWT, lo que permitirá al usuario acceder a las rutas, servicios y recursos permitidos por ese token. El inicio de sesión único es una característica en la que JWT se usa ampliamente hoy en día, ya que tiene pocos gastos generales y se puede usar fácilmente en diferentes dominios.

2. Intercambio de información

Los tokens web JSON son una excelente manera de transferir información de forma segura entre partes. Debido a que los JWT se pueden firmar (por ejemplo, usando un par de claves pública/privada), puede estar seguro de que el remitente es quien dice ser. Además, dado que la firma se calcula utilizando el encabezado y la carga útil, también puedes verificar que el contenido no haya sido manipulado.

7.3 ¿Por qué utilizar JWT?

Basado en la autenticación de sesión tradicional

Insertar descripción de la imagen aquí

defecto:

1. Después de que nuestra aplicación autentica a cada usuario, nuestra aplicación debe realizar un registro en el servidor para facilitar la identificación de la próxima solicitud del usuario. Generalmente, la sesión se guarda en la memoria y, como usuario autenticado, si el número aumenta. , la sobrecarga del lado del servidor aumentará significativamente.

2 Debido a que la identificación del usuario se basa en cookies, si la cookie es interceptada, el usuario será vulnerable a ataques de falsificación de solicitudes entre sitios.

Basado en la autenticación JWT

Insertar descripción de la imagen aquí

Ventajas de jwt:

Compacto: se puede enviar a través de URL, parámetros POST o encabezado HTTP, porque el volumen de datos es pequeño y la velocidad de transmisión es rápida.

Autocontenido: La carga útil contiene toda la información requerida por el usuario, evitando múltiples consultas a la base de datos.

Debido a que el token se almacena en el cliente en formato cifrado JSON, JWT es multilingüe y, en principio, compatible con cualquier formulario web.

6.3 Introducción a la estructura JWT

6.3.1 Composición de tokens

1.Encabezado
2.Carga útil
3.Firma

Por lo tanto, un JWT normalmente tiene este aspecto: xxxxx.yyyyy.zzzzz Header.Payload.Signature

7.2.2 Parte del encabezado

El encabezado normalmente consta de dos partes: el tipo de token (es decir, JWT) y el algoritmo de firma utilizado, como HMAC SHA256 o RSA. Utilizará codificación Base64 para formar la primera parte de la estructura JWT.

Nota: Base64 es una codificación, lo que significa que se puede traducir a su forma original. No es un proceso de cifrado.

{
    
    
  "alg": "HS256",
  "typ": "JWT"
}

6.3.2 Parte de carga útil

La segunda parte del token es la carga útil, que contiene los reclamos. Los reclamos son declaraciones sobre entidades (generalmente usuarios) y otros datos. Asimismo, utilizará codificación Base64 para formar la segunda parte de la estructura JWT.

{
    
    
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}
6.3.3 Parte de la firma

Las dos primeras partes están codificadas usando Base64, es decir, la interfaz puede decodificar y conocer la información que contiene. La firma debe usar el encabezado codificado, la carga útil y una clave que proporcionamos, y luego usar el algoritmo de firma (HS256) especificado en el encabezado para firmar. El propósito de la firma es garantizar que el JWT no haya sido manipulado.

Ejemplo:
HMACSHA256(base64UrlEncode(encabezado) + “.” + base64UrlEncode(carga útil),secreto);

Propósito de la firma

En realidad, el último paso del proceso de firma es firmar el encabezado y el contenido de la carga útil para evitar que el contenido sea manipulado. Si alguien decodifica el contenido del encabezado y la carga útil, lo modifica, luego lo codifica y finalmente agrega la combinación de firma anterior para formar un nuevo JWT, entonces el servidor determinará que la firma formada por el nuevo encabezado y la carga útil está adjunta al JWT: Las firmas son diferentes. Si desea firmar nuevos encabezados y cargas útiles, la firma resultante será diferente si no conoce la clave utilizada por el servidor para el cifrado.

Insertar descripción de la imagen aquí

6.4 Uso de JWT

6.4.1 Introducir dependencias
<!--引入jwt-->
<dependency>
  <groupId>com.auth0</groupId>
  <artifactId>java-jwt</artifactId>
  <version>3.4.0</version>
</dependency>

6.4.2 Generar token
//生成令牌
String token = JWT.create()
  .withClaim("username", "张三")//设置自定义用户名
  .sign(Algorithm.HMAC256("token!Q2W#E$RW"));//设置签名 保密 复杂
//输出令牌
System.out.println(token);

Generar resultados :

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOlsicGhvbmUiLCIxNDMyMzIzNDEzNCJdLCJleHAiOjE1OTU3Mzk0NDIsInVzZXJuYW1lIjoi5byg5LiJIn0.aHmE3RNqvAjFr_dvyn_sD2VJ46P7EGiS5OBMO_TI5jg
6.4.3 Análisis de datos basado en tokens
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("token!Q2W#E$RW")).build();
DecodedJWT decodedJWT = jwtVerifier.verify(token);
System.out.println("用户名: " + decodedJWT.getClaim("username").asString());
6.4.4 Excepciones comunes
- SignatureVerificationException:				签名不一致异常
- TokenExpiredException:    						令牌过期异常
- AlgorithmMismatchException:						算法不匹配异常
- InvalidClaimException:								失效的payload异常
6.4.6 Integración de JWT en RBAC
6.4.6.1 Clase de herramienta de extracción
package cn.wolfcode.util;

/**
 * create By  fjl
 */
@Component
@Getter
@Setter
public class JWTUtils {
    
    

    @Value("${jwt.scret}")
    public  String scret;
    @Value("${jwt.head}")
    public  String head;
  
    public  String createTokenMap(Map<String,String> map) {
    
    

        JWTCreator.Builder builder = JWT.create();
        for (Map.Entry<String, String> entry : map.entrySet())     {
    
    
            builder.withClaim(entry.getKey(), entry.getValue());
        }
        String token = builder.sign(Algorithm.HMAC256(scret));
        return token;
    }
    public  String createToken(String key , String value) {
    
    

        JWTCreator.Builder builder = JWT.create();
        builder.withClaim(key,value);
        String token = builder.sign(Algorithm.HMAC256(scret));
        return token;
    }

   s
    public  String getToken1(String token,String key){
    
    

        //先验证签名
        JWTVerifier verifier = JWT.require(Algorithm.HMAC256(scret)).build();
        //验证其他信息
        DecodedJWT verify = verifier.verify(token);
        String value = verify.getClaim(key).asString();
        return value;
    }
}
6.4.6.2 Agregar configuración
jwt:
  scret: abced
  head: Authencation
6.4.6.3 Modificar LoginServiceImpl
  @Override
    public String login(LoginVO loginVO) {
    
    
        //参数校验
        if(loginVO==null){
    
    
            throw new BusinessException("非法操作");
        }

        if(StringUtils.isEmpty(loginVO.getUsername()) || StringUtils.isEmpty(loginVO.getPassword())){
    
    
            throw new BusinessException("账号密码不能为空");
        }

        if(StringUtils.isEmpty(loginVO.getCode())){
    
    
            throw new BusinessException("验证码不能为空");
        }
        // 从 redis 中获取密码
        String redisCode = redisUtils.get(Constant.VERFI_CODE_PREFIX + loginVO.getUuid());
        boolean flag = VerifyCodeUtil.verification(redisCode, loginVO.getCode(), true);
        if(!flag){
    
    
            throw new BusinessException("验证码不正确");
        }
        // 根据账号密码去查询数据
//        Employee employee = employeeService.login(loginVO.getUsername(),loginVO.getPassword());
//        if(employee == null){
    
    
//            throw new BusinessException("账号密码错误");
//        }
        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(loginVO.getUsername(),loginVO.getPassword());
        Authentication authenticate =
                authenticationManager.authenticate(token);
        LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
        Employee employee = loginUser.getEmployee();

        //创建 token   login_user:uuid
        String uuid = UUID.randomUUID().toString();
        String jwtToken = jwtUtils.createToken1(Constant.JWT_TOKEN_KEY, uuid);

//         把当前登录用户放到 redis 中为了后去判断是否登录做铺垫
//         login_employee:id     employee
        redisUtils.set(Constant.LOGIN_EMPLOYEE+uuid, JSON.toJSONString(employee),Constant.EXPRE_TIME);
//         把当前登录用户所拥有的权限放到 session 中
//         根据当前用户查询 用户拥有权限表达式
        List<String> expressions = permissionService.queryPermissionByEmpId(employee.getId());
        redisUtils.set(Constant.EMPLOYEE_EXPRESSIONS+uuid,JSON.toJSONString(expressions),Constant.EXPRE_TIME);
        return jwtToken;
    }
6.4.6.4 Modificar JwtAuthenticationTokenFilter
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
    
    

    String token = request.getHeader(jwtUtils.getHead());

    if (!StringUtils.isEmpty(token)) {
    
    
        String uuid = jwtUtils.getToken1(token, Constant.JWT_TOKEN_KEY);
        String objJson = redisUtils.get(Constant.LOGIN_EMPLOYEE + uuid);

        if(!StringUtils.isEmpty(objJson)){
    
    
            Employee employee = JSON.parseObject(objJson, Employee.class);
            LoginUser loginUser = new LoginUser();
            loginUser.setEmployee(employee);

            UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(employee.getUsername(), employee.getPassword(), loginUser.getAuthorities());
            SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
        }
    }
    filterChain.doFilter(request, response);

}
6.4.6.5 Modificar front-end main.js
// 请求拦截
axios.interceptors.request.use(function(request){
    
    
      const token = window.sessionStorage.getItem("token");
      if(token){
    
    
        request.headers.Authencation=token;
      }
      return request;
},function(err){
    
    
  return Promise.reject(err)
})
6.4.6.6 Modificar login.js del front-end
 async login() {
    
    
      const {
    
     data: res } = await this.$http.post("login", this.loginForm);
      console.log(res);
      if (res.code != 200) {
    
    
        console.log("登录失败");
      } else {
    
    
        console.log("登录成功");
        window.sessionStorage.setItem("token", res.data);
        this.$router.push("/main");
      }
    },
   }

6.5.6.7 Modificar enrutamiento index.js
router.beforeEach((to,from,next) =>{
    
    
	console.log("router---beforeEach")
	// to 将要访问的路径
	// from 代表从哪个路径跳转而来
	// next 是一个函数,表示放行
	//     next()  放行    next('/login')  强制跳转
	if(to.path==="/login") return next();
	const token=window.sessionStorage.getItem("token");
  console.log(token)
	if(token) return next();
	next("/login")
});

7. Apéndice: elementos de configuración de HttpSecurity

método ilustrar
openidIniciar sesión() Para autenticación basada en OpenId
encabezados() Agregar encabezados de seguridad a la respuesta
cors() Configurar el uso compartido de recursos entre orígenes (CORS)
gestión de sesión() Permitir la configuración de la gestión de sesiones.
puertoMapper() Redirigir a HTTPS o de HTTPS a HTTP. De forma predeterminada, Spring Security utiliza PortMapperImpl para asignar el puerto HTTP 8080 al puerto HTTPS 8443 y el puerto HTTP 80 al puerto HTTPS 443.
jajaja() Configure la autenticación previa basada en contenedores. En este caso, la autenticación la gestiona el contenedor de servlet.
x509() Configurar la autenticación basada en x509
Acuérdate de mí Permite configurar la verificación "recordarme"
autorizarSolicitudes() Permitir acceso restringido según el uso de HttpServletRequest
solicitud de caché() Permitir el almacenamiento en caché de solicitudes de configuración
manejo de excepciones() Permitir el manejo de errores de configuración
contextodeseguridad() Configure la administración de SecurityContext en SecurityContextHolder entre HttpServletRequests. Cuando utilice WebSecurityConfifigurerAdapter, esto
servletApi() Integre el método HttpServletRequest en SecurityContext con los valores que se encuentran en él. Esto se aplicará automáticamente cuando se utilice WebSecurityConfifigurerAdapter
csrf() Agregue soporte CSRF, habilitado de forma predeterminada cuando se usa WebSecurityConfifigurerAdapter
cerrar sesión() Se agregó soporte para cerrar sesión. Esto se aplicará automáticamente cuando se utilice WebSecurityConfifigurerAdapter. De forma predeterminada, acceder a la URL "/logout" invalida la sesión HTTP.
anónimo() Permite la configuración de métodos de representación de usuarios anónimos. Esto se aplica automáticamente cuando se usa junto con WebSecurityConfifigurerAdapter. De forma predeterminada, los usuarios anónimos utilizarán
formulario de inicio de sesión() Especifica la compatibilidad con la autenticación basada en formularios. Si no se especifica FormLoginConfifigurer#loginPage(String), se generará una página de inicio de sesión predeterminada
oauth2Iniciar sesión() Configurar la autenticación contra un proveedor externo OAuth 2.0 u OpenID Connect 1.0
requiereCanal() Configurar la seguridad del canal. Para que esta configuración sea útil, se debe proporcionar al menos una asignación al canal requerido.
httpBásico() Configurar la autenticación básica HTTP
agregarFilterAt() Permitir el manejo de errores de configuración
manejo de excepciones() Agregue un filtro en la ubicación de la clase de filtro especificada

Supongo que te gusta

Origin blog.csdn.net/m0_52896752/article/details/132904915
Recomendado
Clasificación