Lo guiará paso a paso para realizar la plataforma de desarrollo de código bajo: modo de sesión del esquema de tecnología de autenticación de identidad, introducción y comparación del modo token, transformación de front-end e implementación de back-end

Soluciones técnicas

En primer lugar, hablemos de la autenticación de identidad, existen dos soluciones técnicas, una se basa en el modo Sesión tradicional y la otra se basa en el modo token, generalmente usando JWT.

Modo de sesión

El modo de sesión tradicional se ha utilizado durante muchos años y la tecnología es muy madura, pero este modo tiene algunas desventajas:

  1. Consumo de recursos: el uso del modo de sesión requiere el almacenamiento de datos de sesión en el servidor, lo que puede conducir a un mayor consumo de recursos del servidor. Esto puede provocar que el servidor se bloquee o se ralentice si un gran número de usuarios utilizan el sistema al mismo tiempo.

  2. Escalabilidad: el uso del modo de sesión puede afectar la escalabilidad del sistema. Si necesita escalar su sistema a varios servidores, debe asegurarse de que todos los servidores tengan acceso a los datos de la sesión. Esto puede requerir el uso de almacenamiento compartido u otras técnicas, lo que puede agregar complejidad y costo al sistema.

  3. Seguridad: el uso del modo de sesión puede afectar la seguridad del sistema. Si un atacante compromete o manipula los datos de la sesión, el atacante puede obtener acceso a información confidencial del usuario. Por lo tanto, los datos de la sesión deben protegerse con las medidas de seguridad adecuadas, como el cifrado y la autenticación.

  4. Mantenibilidad: el uso del modo de sesión puede afectar la capacidad de mantenimiento del sistema. Si cambia la estructura o el formato de los datos de la sesión, es posible que sea necesario modificar el código del sistema. Esto puede resultar en mayores costos de mantenimiento para el sistema.

El mayor impacto del uso de la sesión es en realidad el segundo punto de escalabilidad. Para evitar un punto único de falla y garantizar una alta disponibilidad en el entorno de producción, la implementación del clúster es una operación de rutina. Cómo garantizar que los usuarios puedan operar normalmente después de iniciar sesión, por lo general, hay varias soluciones:
1. Permanencia de la sesión: el equilibrio de carga implementa la permanencia de la sesión a través del proxy inverso, es decir, de acuerdo con ciertas reglas, como la dirección IP, un usuario siempre se enruta a un nodo de clúster fijo.
2. Almacenamiento centralizado: use Redis, base de datos, etc. para almacenar información de sesión de manera centralizada, a fin de compartir datos de sesión en múltiples nodos en el clúster.
3. Replicación de sesiones: Sincronice los datos de la sesión de un nodo en el clúster con otros nodos del clúster. Rara vez se usa porque es complicado, propenso a errores y traerá tráfico de red adicional.
Además, en el caso del acceso a terminales móviles, no existe un mecanismo de sesión web tradicional. En este momento, se necesitan componentes funcionales de terceros, como Spring Session, para simular y generar información de sesión similar a la web, lo que aumenta aún más la complejidad. .

modo token

Con la aparición y el desarrollo del modelo de arquitectura de separación front-end y back-end, y la popularidad de los microservicios, el enfoque actual basado en tokens se ha convertido gradualmente en la solución técnica principal. El proceso es el siguiente:

  1. Un usuario ingresa un nombre de usuario y contraseña en la aplicación de front-end y se envía a la API de back-end.
  2. La API de backend verifica las credenciales del usuario y genera un token. Los tokens generalmente contienen algunos metadatos, como el tiempo de vencimiento y la identificación del usuario.
  3. La API de backend envía el token de regreso a la aplicación de frontend.
  4. La aplicación frontal almacena el token en el almacenamiento local o en una cookie.
  5. En solicitudes posteriores, la aplicación de front-end envía el token a la API de back-end para demostrar que el usuario se ha autenticado.
  6. La API de back-end verifica la validez del token y toma las medidas adecuadas según sea necesario.
    En este proceso, el token es la clave para la autenticación. Los tokens generalmente se codifican utilizando el formato JSON Web Token (JWT), un estándar abierto para transferir información de forma segura entre las partes. El JWT contiene una firma para garantizar que el token no haya sido manipulado.

El modo token tiene principalmente las siguientes ventajas:

  1. Seguridad: los tokens son un mecanismo de autenticación seguro porque contienen información firmada y cifrada para garantizar que el token no haya sido manipulado. Esto hace que el token sea más seguro que la autenticación tradicional basada en cookies.
  2. Escalabilidad: los tokens son un mecanismo de autenticación escalable porque se pueden compartir entre varias aplicaciones. Esto hace que los tokens sean más flexibles que la autenticación tradicional basada en cookies.
  3. Sin estado: los tokens son un mecanismo de autenticación sin estado, ya que no requieren que se almacene ninguna información en el lado del servidor. Esto hace que los tokens sean más simples y fáciles de mantener que la autenticación tradicional basada en sesiones.
    Entre los puntos anteriores, la falta de estado lo hace especialmente adecuado para la expansión flexible del backend, como agregar nodos de clúster.

realización de funciones

Transformación frontal

Iniciar una solicitud de inicio de sesión

La operación de inicio de sesión del marco de front-end vue-element-plus-admin se encuentra en src/views/Login/components/LoginForm.vue, y la llamada predeterminada es datos simulados. La dirección de la interfaz API de inicio de sesión real del backend de la plataforma es /system/user/login, así que modifique la dirección URL en el método loginApi en api/login/index.ts

export const loginApi = (data: UserType) => {
    
    
  return request.post({
    
    
    url: '/system/user/login?username=' + data.username + '&password=' + data.password,
    data
  })
}

Guardar token después de un inicio de sesión exitoso

Primero, debe guardar el token devuelto por la API de solicitud de inicio de sesión.
Con respecto a guardar la información del usuario después de un inicio de sesión exitoso, la implementación original del marco es la siguiente:

// 登录
const signIn = async () => {
  const formRef = unref(elFormRef)
  await formRef?.validate(async (isValid) => {
    if (isValid) {
      loading.value = true
      const { getFormData } = methods
      const formData = await getFormData<UserType>()

      try {
        const res = await loginApi(formData)

        if (res) {
          wsCache.set(appStore.getUserInfo, res.data)
          // 是否使用动态路由
          if (appStore.getDynamicRouter) {
            getRole()
          } else {
            await permissionStore.generateRoutes('none').catch(() => {})
            permissionStore.getAddRouters.forEach((route) => {
              addRoute(route as RouteRecordRaw) // 动态添加可访问路由表
            })
            permissionStore.setIsAddRouters(true)
            push({ path: redirect.value || permissionStore.addRouters[0].path })
          }
        }
      } finally {
        loading.value = false
      }
    }
  })
}

El código anterior proviene de src/views/Login/components/LoginForm.vue. La declaración clave es la línea 14. Usando la clase de herramienta de caché, los datos devueltos por el backend, es decir, la información del usuario, se almacenan en SessionStorage.

Este lugar en realidad no usa la gestión de estado global de vue. Hice la modificación de la siguiente manera:

import { useUserStore } from '@/store/modules/user'
const userStore = useUserStore()

  
const res = await loginApi(formData)
if (res) {
   // 保存用户信息
   userStore.setUserAction(res.data)
……

Luego agregue user.ts en el directorio src\store\modules, incluidos los campos clave como el logotipo, el número de cuenta, el nombre, si se debe forzar la modificación de la contraseña, el token, la matriz de permisos de menú y la matriz de permisos de botones. El código es el siguiente:

import { store } from '../index'
import { defineStore } from 'pinia'
import { useCache } from '@/hooks/web/useCache'
import { USER_KEY } from '@/constant/common'
const { wsCache } = useCache()
import { setToken } from '@/utils/auth'

interface UserState {
  account: string
  name: string
  forceChangePassword: string
  id: string
  token: string
  buttonPermission: string[]
  menuPermission: string[]
}

export const useUserStore = defineStore('user', {
  state: (): UserState => ({
    account: '',
    name: '',
    forceChangePassword: '',
    id: '',
    token: '',
    buttonPermission: [],
    menuPermission: []
  }),
  getters: {
    getAccount(): string {
      return this.account
    }
  },
  actions: {
    async setUserAction(user) {
      this.account = user.account
      this.name = user.name
      this.forceChangePassword = user.forceChangePassword
      this.id = user.id
      this.token = user.token
      this.buttonPermission = user.buttonPermission
      this.menuPermission = user.menuPermission
      // 保存用户信息
      wsCache.set(USER_KEY, user)
      // 保存令牌
      setToken(user.token)
    },
    async clear() {
      wsCache.clear()
      this.resetState()
    },
    resetState() {
      this.account = ''
      this.name = ''
      this.forceChangePassword = ''
      this.id = ''
      this.token = ''
      this.buttonPermission = []
      this.menuPermission = []
    }
  }
})

export const useUserStoreWithOut = () => {
  return useUserStore(store)
}

La línea 43 guarda toda la información del usuario. Teniendo en cuenta que el token se usará con frecuencia, no es eficiente obtener el objeto de usuario completo y luego obtener el token cada vez, por lo que el token se guarda por separado, línea 45. Una clase de herramienta de lectura y escritura de tokens encapsulada aquí.

import {
    
     useCache } from '@/hooks/web/useCache'
import {
    
     TOKEN_KEY } from '@/constant/common'
const {
    
     wsCache } = useCache()


// 获取token
export const getToken = () => {
    
    
  return wsCache.get(TOKEN_KEY) ? wsCache.get(TOKEN_KEY) : ''
}


// 设置token
export const setToken = (token) => {
    
    
  wsCache.set(TOKEN_KEY, token)
}


// 删除token
export const removeToken = () => {
    
    
  wsCache.delete(TOKEN_KEY)
}

Después de un inicio de sesión exitoso, use la función de depuración del navegador para ver SessionStorage, y verá que tanto la información del usuario como la información del token se han guardado.
imagen.png

Traiga el token para acceder al backend

El front-end solicita los axios utilizados por el back-end, lo que requiere modificar la configuración de axios, agregar un token de lectura al interceptor de solicitudes y establecer el método de encabezado. La ubicación del código correspondiente es src\config\axios\service.ts, de la siguiente manera:

// 创建axios实例
const service: AxiosInstance = axios.create({
    
    
  baseURL: PATH_URL, // api 的 base_url
  timeout: config.request_timeout // 请求超时时间
})
// request拦截器
service.interceptors.request.use(
  (config: InternalAxiosRequestConfig) => {
    
    
    // 读取token
    const token = getToken()
    if (token) {
    
    
      // 若不为空,则将token放入header属性
      config.headers['X-Token'] = token
    }
    const urlencoded = 'application/x-www-form-urlencoded'
    if (
      config.method === 'post' &&
      (config.headers as AxiosRequestHeaders)['Content-Type'] === urlencoded
    ) {
    
    
      config.data = qs.stringify(config.data)
    }  
    ……
    return config
  },
  (error: AxiosError) => {
    
    
    // Do something with request error
    console.log(error) // for debug
    Promise.reject(error)
  }
)

Líneas 9-14 mi código agregado.
En este momento, en la situación de inicio de sesión, inicie cualquier solicitud, use la función de depuración del navegador y podrá ver que hay más información de token en el encabezado.
imagen.png

Manejo de invalidación de tokens

Después de recibir la solicitud del front-end, el back-end verificará el token. Si la verificación falla, el token en sí no es válido o se agotó el tiempo de espera. En este momento, se devuelve un código de estado 401 Http al frente. -end El front-end necesita usar este estado El código brinda indicaciones fáciles de usar y conduce a la página de inicio de sesión del sistema.

En este momento, lo que debe ajustarse es la configuración de axios, y el juicio del código de estado y las indicaciones amigables se realizan en el interceptor de respuesta.

// response 拦截器
service.interceptors.response.use(
  (response: AxiosResponse<any>) => {
    
    
    if (response.config.responseType === 'blob') {
    
    
      // 如果是文件流,直接过
      return response
    } else if (response.status === REQUEST_SUCCESS) {
    
    
      return new Promise((resolve) => {
    
    
        // 若为成功请求,直接返回业务数据
        if (response.status === REQUEST_SUCCESS) {
    
    
          resolve(response)
        }
      })
      return response.data
    } else {
    
    
      ElMessage.error(response.data.message)
    }
  },
  (error: AxiosError) => {
    
    
    if (error.response) {
    
    
      if (error.response.status === UNAUTHORIZED) {
    
    
        // 收到401响应时,给出友好提示
        ElMessage.warning('未登录或会话超时,请重新登录')
        // 清空浏览器缓存
        wsCache.clear()
        // 执行页面刷新
        setTimeout(function () {
    
    
          location.reload()
        }, 2000)
      } else if (error.response.status === NOT_FOUND) {
    
    
        ElMessage.error('未找到服务,请确认')
      } else if (error.response.status === METHOD_NOT_ALLOWED) {
    
    
        ElMessage.error('请求的方法不支持,请确认')
      } else {
    
    
        ElMessage.error(error.response.data.message)
      }
      return Promise.reject(error)
    } else {
    
    
      ElMessage.error('请求远程服务器失败')
    }
  }
)

Para el procesamiento del código de estado 401, consulte las líneas anteriores 21 a 29. Para 404 y 405, el método de procesamiento es similar.

Cuando se recibe una solicitud 401, se le dará un recordatorio amistoso en la parte superior central del sistema, y ​​automáticamente saltará a la página de inicio de sesión después de 2 segundos.
imagen.png

Además, los estados de http comunes incluyen 400 y 403. Estos dos estados no los activa el usuario de la forma normal del sistema operativo. A menudo son causados ​​por el uso de herramientas de depuración de interfaz, o por copiar y pegar directamente la dirección URL en el navegador. Correspondiendo a estos dos modos, el backend devolverá un código de estado 200, pondrá la información del error en la respuesta y el frontend dará un aviso amistoso.

Limpiar tokens después de cerrar sesión en el sistema

Después de que se cierra la sesión del sistema, se debe borrar el token. El propio framework ya lo ha hecho, y todos los cachés se borran directamente. El código se encuentra en la línea 10 de src\components\UserInfo\src\UserInfo.vue

const loginOut = () => {
    
    
  ElMessageBox.confirm(t('common.loginOutMessage'), t('common.reminder'), {
    
    
    confirmButtonText: t('common.ok'),
    cancelButtonText: t('common.cancel'),
    type: 'warning'
  })
    .then(async () => {
    
    
      const res = await loginOutApi().catch(() => {
    
    })
      if (res) {
    
    
        wsCache.clear()
        tagsViewStore.delAllViews()
        resetRouter() // 重置静态路由表
        replace('/login')
      }
    })
    .catch(() => {
    
    })
}

implementación de back-end

La plataforma integra componentes de Spring Security para lograr la autenticación de identidad y el control de autoridad. Hoy, nos centraremos en las funciones relacionadas con la autenticación de identidad. Hay muchos contenidos sobre cómo integrar Spring Security, y presentaré un artículo especial más adelante.

API de inicio de sesión del sistema

El componente Spring Security tiene una función de inicio de sesión incorporada, solo necesita escribir una clase de configuración y realizar la configuración necesaria.
Si solo coloca fragmentos de código, es difícil ver claramente las dependencias y la lógica del código. Primero coloque el archivo de configuración general a continuación y luego concéntrese en las funciones que se mencionarán.

package tech.abc.platform.framework.security;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.expression.SecurityExpressionOperations;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.Authentication;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler;
import org.springframework.security.web.access.expression.WebSecurityExpressionRoot;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsUtils;

/**
 * SpringSecurity安全框架配置
 *
 * @author wqliu
 * @date 2023-03-08
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
    
    

    /**
     * 登录处理地址
     */
    public static final String SYSTEM_USER_LOGIN = "/system/user/login";
    /**
     * 注销处理地址
     */
    public static final String SYSTEM_USER_LOGOUT = "/system/user/logout";
    /**
     * 会话超时地址
     */
    public static final String SYSTEM_USER_SESSION_INVALID = "/system/user/sessionInvalid";


    @Autowired
    private UserDetailsServiceImpl myUserService;

    @Autowired
    private AuthenticationSuccessHandler myAuthenticationSuccessHandler;

    @Autowired
    private AuthenticationFailureHandler myAuthenticationFailHandler;

    @Autowired
    private MyLogoutHandler myLogoutHandler;

    @Autowired
    private MyLogoutSuccessHandler myLogoutSuccessHandler;

    @Autowired
    private MyPermissionEvaluator myPermissionEvaluator;

    @Autowired
    private JwtFilter jwtFilter;


    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    
    
        // 设置自定义用户服务及加密方式
        auth.userDetailsService(myUserService).passwordEncoder(new BCryptPasswordEncoder());
    }


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

        // 允许跨域访问
        http.cors();
        // 禁用csrf攻击防护
        http.csrf().disable();
        // 登录处理
        http.formLogin()
                // 此处的方法实际是虚拟的,并不需要在UserControl控制器中存在,与前端请求一致即可,会被SpringSecurity截获
                // 即使该方法存在,也会被SpringSecurity优先截获
                // 另外,前后端分离的情况下,不需要指定登录地址loginPage参数,指定了也不起作用
                .loginProcessingUrl(SYSTEM_USER_LOGIN)
                // 设置自定义的身份认证成功处理器
                .successHandler(myAuthenticationSuccessHandler)
                // 设置自定义的身份认证失败处理器
                .failureHandler(myAuthenticationFailHandler);

        // 注销处理
        http.logout()
                // 这里新加自定义处理处理器主要是生成用户注销审计日志,如放在logoutSuccessHandler则无法取到当前用户
                .addLogoutHandler(myLogoutHandler)
                .logoutUrl(SYSTEM_USER_LOGOUT)
                .logoutSuccessHandler(myLogoutSuccessHandler)
                .invalidateHttpSession(true);


        // 会话管理
        http.sessionManagement()
                // 使用jwt token,不需要session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS);


        // 配置允许访问页面
        http.authorizeRequests()
                // 允许跨域请求中的Preflight请求
                .requestMatchers(CorsUtils::isPreFlightRequest).permitAll()

                // 允许swagger文档接口匿名访问
                .antMatchers("/swagger-ui.html").anonymous()
                .antMatchers("/swagger-resources/**").anonymous()
                .antMatchers("/webjars/**").anonymous()
                .antMatchers("/*/api-docs").anonymous()


        ;

        // 配置其他请求,需认证
        http.authorizeRequests()
                .anyRequest()
                .authenticated();

        // 配置JWT过滤器
        http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);


    }


    @Override
    public void configure(WebSecurity web) {
    
    
        web.expressionHandler(new DefaultWebSecurityExpressionHandler() {
    
    
            @Override
            protected SecurityExpressionOperations createSecurityExpressionRoot(Authentication authentication, FilterInvocation fi) {
    
    
                WebSecurityExpressionRoot root = (WebSecurityExpressionRoot) super.createSecurityExpressionRoot(authentication, fi);
                root.setPermissionEvaluator(myPermissionEvaluator);
                return root;
            }
        });
    }
}

La configuración principal de la operación de inicio de sesión es la siguiente:

  /**
   * 登录处理地址
   */
  public static final String SYSTEM_USER_LOGIN = "/system/user/login";


// 登录处理
  http.formLogin()
          // 此处的方法实际是虚拟的,并不需要在UserControl控制器中存在,与前端请求一致即可,会被SpringSecurity截获
          // 即使该方法存在,也会被SpringSecurity优先截获
          // 另外,前后端分离的情况下,不需要指定登录地址loginPage参数,指定了也不起作用
          .loginProcessingUrl(SYSTEM_USER_LOGIN)
          // 设置自定义的身份认证成功处理器
          .successHandler(myAuthenticationSuccessHandler)
          // 设置自定义的身份认证失败处理器
          .failureHandler(myAuthenticationFailHandler);

Después de la configuración anterior, la dirección de la interfaz API de la operación de inicio de sesión expuesta por el backend al frontend es /system/user/login.

El inicio de sesión genera correctamente el token

El componente SpringSecurity devolverá la llamada a un método de la clase AuthenticationSuccessHandler después de que la autenticación sea exitosa. Después de un inicio de sesión exitoso, configure la información del usuario, incluida la identificación, el número de cuenta, el nombre, si se debe forzar el cambio de contraseña, los datos del menú, los permisos de los botones y la generación de tokens, todo se implementa aquí.

package tech.abc.platform.framework.security;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import tech.abc.platform.common.annotation.SystemLog;
import tech.abc.platform.common.constant.CommonConstant;
import tech.abc.platform.common.constant.TreeDefaultConstant;
import tech.abc.platform.common.entity.MyUserDetails;
import tech.abc.platform.common.enums.LogTypeEnum;
import tech.abc.platform.common.enums.YesOrNoEnum;
import tech.abc.platform.common.utils.CacheUtil;
import tech.abc.platform.common.utils.JwtUtil;
import tech.abc.platform.common.utils.ResultUtil;
import tech.abc.platform.common.vo.Result;
import tech.abc.platform.framework.config.PlatformConfig;
import tech.abc.platform.system.entity.PermissionItem;
import tech.abc.platform.system.entity.User;
import tech.abc.platform.system.enums.PermissionTypeEnum;
import tech.abc.platform.system.service.OrganizationService;
import tech.abc.platform.system.service.UserService;
import tech.abc.platform.system.vo.MenuTreeVO;
import tech.abc.platform.system.vo.MetaVO;
import tech.abc.platform.system.vo.UserVO;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

/**
 * 自定义登录成功处理器
 * 如继承父类则会内置跳转首页或权限验证失败前一页,会影响前端跳转,自己实现接口,去除了后端自动跳转
 *
 * @author wqliu
 * @date 2023-03-08
 */
@Component
@Slf4j
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    
    

    public static final double VALIDATE_LICENCE_PERCENT = 0.2;
    @Autowired
    private UserService userService;

    @Autowired
    private OrganizationService organizationService;

    @Autowired
    private CacheUtil cacheUtil;


    @Autowired
    private PlatformConfig platformConfig;

    @Autowired
    private JwtUtil jwtUtil;


    @Override
    @SystemLog(value = "登录成功", logType = LogTypeEnum.AUDIT, logRequestParam = false, executeResult = CommonConstant.YES)
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                        Authentication authentication)
            throws IOException {
    
    


        MyUserDetails userDetails = (MyUserDetails) authentication.getPrincipal();
        // 获取用户名
        String account = userDetails.getUsername().toLowerCase();

        // 查询用户信息
        QueryWrapper<User> userQueryWrapper = new QueryWrapper<>();
        userQueryWrapper.lambda().eq(User::getAccount, account);
        User user = userService.getOne(userQueryWrapper);

        // 重置登录失败次数
        userService.resetLoginFailureCount(user.getId());


        // 构造返回对象
        UserVO userVO = new UserVO();
        userVO.setId(user.getId());
        userVO.setAccount(user.getAccount());
        userVO.setName(user.getName());

        // 强制修改密码标志位
        userVO.setForceChangePasswordFlag(user.getForceChangePasswordFlag());
        // 判断是否超出密码修改时间
        if (userService.checkExceedPasswordChangeDays(user.getId())) {
    
    
            userVO.setForceChangePasswordFlag(YesOrNoEnum.YES.name());
        }
        // 生成令牌
        String token = jwtUtil.generateTokenWithSubject(user.getAccount(), platformConfig.getSystem().getTokenValidSpan() * 60);
        // 设置令牌
        userVO.setToken(token);


        // 获取权限
        List<PermissionItem> permissionList = userService.getPermission(user.getId());
        if (CollectionUtils.isNotEmpty(permissionList)) {
    
    
            // 获取按钮权限
            List<String> buttonPermission = permissionList.stream().filter(x -> x.getType()
                            .equals(PermissionTypeEnum.BUTTON.toString()))
                    .map(PermissionItem::getPermissionCode).distinct().collect(Collectors.toList());
            userVO.setButtonPermission(buttonPermission);
            // 获取菜单权限
            List<MenuTreeVO> moduleList = getMenu(permissionList);
            userVO.setMenuPermission(moduleList);
        }
        // 构建返回
        ResponseEntity<Result> result = ResultUtil.success(userVO, "登录成功");
        ResultUtil.returnJsonToFront(response, result);

    }


    /**
     * 获取菜单
     * 目前支持两级
     *
     * @return
     */
    public List<MenuTreeVO> getMenu(List<PermissionItem> permissionList) {
    
    

        // 生成模块
        List<MenuTreeVO> moduleList
                = generateModule(permissionList, TreeDefaultConstant.DEFAULT_TREE_ROOT_ID);

        for (MenuTreeVO module : moduleList) {
    
    
            // 生成菜单
            List<MenuTreeVO> menus = generateMenu(permissionList, module.getId());
            module.setChildren(menus);
        }
        return moduleList;
    }


    /**
     * 生成模块
     */
    private List<MenuTreeVO> generateModule(List<PermissionItem> list, String parentId) {
    
    
        List<MenuTreeVO> result = new ArrayList<>();
        for (PermissionItem node : list) {
    
    
            // 获取类型为模块权限项
            if (node.getType().equals(PermissionTypeEnum.MODULE.toString())) {
    
    
                if (node.getPermissionItem().equals(parentId)) {
    
    
                    MenuTreeVO vo = new MenuTreeVO();
                    vo.setId(node.getId());
                    vo.setParentId(node.getPermissionItem());
                    vo.setName(node.getCode());
                    vo.setPath("/" + node.getCode());
                    vo.setComponent(node.getComponent());

                    MetaVO metaVO = new MetaVO();
                    metaVO.setTitle(node.getName());
                    metaVO.setIcon(node.getIcon());
                    metaVO.setHidden(false);
                    vo.setMeta(metaVO);
                    result.add(vo);
                }
            }
        }
        return result;
    }

    /**
     * 生成菜单
     */
    private List<MenuTreeVO> generateMenu(List<PermissionItem> list, String parentId) {
    
    
        List<MenuTreeVO> menus = new ArrayList<MenuTreeVO>();

        List<PermissionItem> permissionList
                = list.stream().filter(x -> x.getPermissionItem().equals(parentId)).collect(Collectors.toList());
        for (PermissionItem permission : permissionList) {
    
    

            if (permission.getType().equals(PermissionTypeEnum.MENU.toString())
                    || permission.getType().equals(PermissionTypeEnum.PAGE.toString())) {
    
    
                MenuTreeVO vo = new MenuTreeVO();
                vo.setId(permission.getId());
                vo.setParentId(permission.getPermissionItem());
                vo.setName(permission.getCode());
                vo.setPath(permission.getCode());
                vo.setComponent(permission.getComponent());


                MetaVO metaVO = new MetaVO();
                metaVO.setTitle(permission.getName());
                metaVO.setIcon(permission.getIcon());
                // 如果为页面,非菜单,则设置隐藏
                metaVO.setHidden(permission.getType().equals(PermissionTypeEnum.PAGE.toString()));

                vo.setMeta(metaVO);
                menus.add(vo);
                // 查找下级
                // 菜单规划为两级,因此此处未使用递归,而是再往下找一级即可
                List<MenuTreeVO> children = generateMenu(list, permission.getId());
                if (children != null && children.size() > 0) {
    
    
                    menus.addAll(children);
                }

            }
        }
        return menus;
    }


}

La generación y verificación de tokens encapsula una clase de herramienta.

package tech.abc.platform.common.utils;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import tech.abc.platform.common.exception.SessionExpiredException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.util.Date;

/**
 * jwt工具类
 *
 * @author wqliu
 * @date 2023-03-08
 */
@Component
public class JwtUtil {
    
    

    /**
     * 密钥
     */
    @Value("${platform-config.system.tokenSecret}")
    private String secret;


    /**
     * 默认超时时间 30分钟
     */
    private final Long JWT_DEFAULT_EXPIRE_SECONDS = 30 * 60L;


    /**
     * 获取超时时间
     *
     * @param validSpan 时长,单位 秒
     * @return
     */
    private Date getExpireTime(long validSpan) {
    
    

        // 生成JWT过期时间
        long nowMilliSecond = System.currentTimeMillis();
        if (validSpan < 0) {
    
    
            validSpan = JWT_DEFAULT_EXPIRE_SECONDS;
        }
        long expMilliSecond = nowMilliSecond + validSpan * 1000;
        Date exp = new Date(expMilliSecond);
        return exp;
    }

    /**
     * 生成带主题的令牌
     *
     * @param subject   主题
     * @param validSpan 有效时长,单位秒
     * @return jwt令牌
     */
    public String generateTokenWithSubject(String subject, long validSpan) {
    
    
        Algorithm algorithm = Algorithm.HMAC256(secret);
        Date expireTime = getExpireTime(validSpan);

        String token = JWT.create()
                .withSubject(subject)
                .withExpiresAt(expireTime)
                .sign(algorithm);
        return token;

    }

    /**
     * 验证令牌
     *
     * @param token
     * @return
     */
    public void verifyToken(String token) {
    
    
        Algorithm algorithm = Algorithm.HMAC256(secret);

        JWTVerifier verifier = JWT.require(algorithm)
                .build();
        try {
    
    
            verifier.verify(token);
        } catch (Exception ex) {
    
    
            throw new SessionExpiredException("令牌无效或过期,请重新登录");
        }
    }


    /**
     * 解码令牌
     *
     * @param token
     * @return
     */
    public DecodedJWT decode(String token) {
    
    
        return JWT.decode(token);
    }

    /**
     * 获取主题
     *
     * @param token 令牌
     * @return 主题
     */
    public String getSubject(String token) {
    
    
        return decode(token).getSubject();
    }

}

Autenticación

El siguiente paso es la implementación de la parte central, es decir, la autenticación de identidad. Después de un inicio de sesión exitoso, el front-end llevará un token para cada solicitud posterior. ¿Cómo puede el back-end obtener y verificar este token?
El componente Spring Security en sí mismo no proporciona directamente un modo de inicio de sesión basado en token, pero proporciona un marco que debe ampliarse para implementar un filtro.

package tech.abc.platform.framework.security;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.GenericFilterBean;
import tech.abc.platform.common.utils.JwtUtil;
import tech.abc.platform.common.utils.ResultUtil;
import tech.abc.platform.common.vo.Result;
import tech.abc.platform.framework.config.PlatformConfig;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 基于jwt令牌的身份认证过滤器
 *
 * @author wqliu
 * @date 2023-03-08
 */
@Slf4j
@Component
public class JwtFilter extends GenericFilterBean {
    
    


    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private PlatformConfig platformConfig;

    @Autowired
    private JwtUtil jwtUtil;

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
    
    


        HttpServletRequest req = (HttpServletRequest) servletRequest;


        // 优先从http头中获取令牌
        String token = req.getHeader("X-Token");
        // 其次从cookie中获取
        if (StringUtils.isBlank(token)) {
    
    
            Cookie[] cookies = ((HttpServletRequest) servletRequest).getCookies();
            if (cookies != null) {
    
    
                for (int i = 0; i < cookies.length; i++) {
    
    
                    if ("token".equals(cookies[i].getName())) {
    
    
                        token = cookies[i].getValue();
                        break;
                    }
                }
            }
        }
        // 再次,从url地址中获取
        if (StringUtils.isBlank(token)) {
    
    
            token = req.getParameter("X-Token");
        }


        if (StringUtils.isNotBlank(token)) {
    
    
            // 验证令牌
            try {
    
    
                jwtUtil.verifyToken(token);
            } catch (Exception ex) {
    
    
                ResponseEntity<Result> result = ResultUtil.error(ex.getMessage(), HttpStatus.UNAUTHORIZED);
                ResultUtil.returnJsonToFront((HttpServletResponse) servletResponse, result);
                return;
            }

            // 获取账号
            String account = jwtUtil.decode(token).getSubject();
            // 查询用户信息
            UserDetails user = userDetailsService.loadUserByUsername(account);
            // 构造SpringSecurity的认证对象后放到SecurityContextHolder中
            UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
            SecurityContextHolder.getContext().setAuthentication(authentication);


        }
        // 执行后续过滤器
        filterChain.doFilter(servletRequest, servletResponse);
    }
}

El filtro debe configurarse en la posición adecuada de la cadena de filtros SpringSecurity

// 配置JWT过滤器
http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);

Al mismo tiempo, configure la gestión de la sesión. Dado que se ha utilizado el modo token, la sesión ya no es necesaria. Establezca la política de gestión de la sesión de Spring Security en SessionCreationPolicy.STATELESS.

// 会话管理
  http.sessionManagement()
          // 使用jwt token,不需要session
          .sessionCreationPolicy(SessionCreationPolicy.STATELESS);

En el proceso de verificación del token, si la verificación falla, el token en sí no es válido o se agotó el tiempo de espera. En este momento, se devuelve uniformemente un código de estado 401 Http al front-end, y el front-end le da al usuario una mensaje amigable basado en el código de estado y conduce a la página de inicio de sesión del sistema.

// 验证令牌
try {
    
    
    jwtUtil.verifyToken(token);
} catch (Exception ex) {
    
    
    ResponseEntity<Result> result = ResultUtil.error(ex.getMessage(), HttpStatus.UNAUTHORIZED);
    ResultUtil.returnJsonToFront((HttpServletResponse) servletResponse, result);
    return;
}

API de cierre de sesión del sistema

Similar a la API de inicio de sesión, también se genera a través de la configuración.

/**
 * 注销处理地址
 */
public static final String SYSTEM_USER_LOGOUT = "/system/user/logout";

// 注销处理
  http.logout()
          // 这里新加自定义处理处理器主要是生成用户注销审计日志,如放在logoutSuccessHandler则无法取到当前用户
          .addLogoutHandler(myLogoutHandler)
          .logoutUrl(SYSTEM_USER_LOGOUT)
          .logoutSuccessHandler(myLogoutSuccessHandler)
          .invalidateHttpSession(true);

Información de la plataforma de desarrollo

Nombre de la plataforma: Plataforma de desarrollo One Two Three
Introducción: Plataforma de desarrollo general de nivel empresarial
Información de diseño: columna csdn
Dirección de código abierto: Protocolo de código abierto de Gitee : MIT da la bienvenida a favoritos, me gusta y comentarios Su apoyo es la fuerza que me impulsa a seguir adelante .

Supongo que te gusta

Origin blog.csdn.net/seawaving/article/details/130534967
Recomendado
Clasificación