【若依框架】登录,token,自定义session,鉴权等前后端流程解读

背景

之前虽然讲了login,getInfo,getRoutes的三个接口,但从设计的角度来讲,这3个接口并没有完整实现一个功能。这里重点讲解若依框架对于自定义session,token校验,权限验证三个方面的实现。这些对于自己实现一个简单的后端框架有不错的参考意义

功能说明

  • 登录功能\login及token的生成
  • 权限过滤校验
  • 自定义session
  • 前端如何配合
    可以参考上一篇博客

登录及token生成

主要解决的是用户登录、生成token和session的场景

  1. 前端

用户登录页输入username,password,code(验证码),提交login接口
注:省略获取验证码接口和rememberMe的cookie使用

SysLoginController

# 验证码验证

# 2 登录验证
  // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
            authentication = authenticationManager
                    .authenticate(new UsernamePasswordAuthenticationToken(username, password));
注:上面会把LoginUser注入到Security的Principal中                    

# 3 生成token

 LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        // 生成token
        return tokenService.createToken(loginUser);
                    

TokenService
生成token包含了自定义session和返回token两大部分

  1. 使用UUID作为sessionId

由于作者使用了token这个命名会给阅读带来很大困扰,这里我们理解为sessionId是最准确的。

  public String createToken(LoginUser loginUser)
    {
    
       #这里的token准确的说法是sessionID
       
        String token = IdUtils.fastUUID();
        loginUser.setToken(token);
        setUserAgent(loginUser);
        refreshToken(loginUser);

        Map<String, Object> claims = new HashMap<>();
        claims.put(Constants.LOGIN_USER_KEY, token);
        return createToken(claims);
    }

以sessionId为key,LoginUser为Value保存redis,这个过程本质上就是自定义session。
注:忽略用户信息填充的过程。

  /**
     * 刷新令牌有效期
     * 
     * @param loginUser 登录信息
     */
    public void refreshToken(LoginUser loginUser)
    {
        loginUser.setLoginTime(System.currentTimeMillis());
        loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);
        // 根据uuid将loginUser缓存
        String userKey = getTokenKey(loginUser.getToken());
        redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES);
    }

生成JwtToken

这里产生了真正的Jwttoken返回给了前端

  1. 没有过期时间
  2. 保存了sessionId,方便关联session
    private String createToken(Map<String, Object> claims)
    {
        String token = Jwts.builder()
                .setClaims(claims)
                .signWith(SignatureAlgorithm.HS512, secret).compact();
        return token;
    }

总结:

  1. 验证验证码
  2. 验证登录信息
  3. 生成sessionId
  4. 以sessionId为key,保存LoginUser的redis,有效期为30分钟
  5. 生成包含sessionId的token
  6. 返回给前端JwtToken

注意4,用户登录后在redis实现了自定义session,有效期为expireTime。如果redis中的sessionId过期了,就代表用户session失效。淡然,如果token不对会跑出校验异常,也无法通过,接下来会详细介绍。

接口请求时鉴权过滤

安全配置

这里配置了可以匿名登录的基本信息

  • AuthenticationEntryPointImpl 认证失败处理类
  • LogoutSuccessHandlerImpl 登出处理类
  • JwtAuthenticationTokenFilter权限过滤器,核心

/**
 * spring security配置
 * 
 * @author ruoyi
 */
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter
{
    /**
     * 自定义用户认证逻辑
     */
    @Autowired
    private UserDetailsService userDetailsService;
    
    /**
     * 认证失败处理类
     */
    @Autowired
    private AuthenticationEntryPointImpl unauthorizedHandler;

    /**
     * 退出处理类
     */
    @Autowired
    private LogoutSuccessHandlerImpl logoutSuccessHandler;

    /**
     * token认证过滤器
     */
    @Autowired
    private JwtAuthenticationTokenFilter authenticationTokenFilter;
    
    /**
     * 跨域过滤器
     */
    @Autowired
    private CorsFilter corsFilter;
    
    /**
     * 解决 无法直接注入 AuthenticationManager
     *
     * @return
     * @throws Exception
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception
    {
        return super.authenticationManagerBean();
    }

    /**
     * anyRequest          |   匹配所有请求路径
     * access              |   SpringEl表达式结果为true时可以访问
     * anonymous           |   匿名可以访问
     * denyAll             |   用户不能访问
     * fullyAuthenticated  |   用户完全认证可以访问(非remember-me下自动登录)
     * hasAnyAuthority     |   如果有参数,参数表示权限,则其中任何一个权限可以访问
     * hasAnyRole          |   如果有参数,参数表示角色,则其中任何一个角色可以访问
     * hasAuthority        |   如果有参数,参数表示权限,则其权限可以访问
     * hasIpAddress        |   如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
     * hasRole             |   如果有参数,参数表示角色,则其角色可以访问
     * permitAll           |   用户可以任意访问
     * rememberMe          |   允许通过remember-me登录的用户访问
     * authenticated       |   用户登录后可访问
     */
    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception
    {
        httpSecurity
                // CSRF禁用,因为不使用session
                .csrf().disable()
                // 认证失败处理类
                .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
                // 基于token,所以不需要session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                // 过滤请求
                .authorizeRequests()
                // 对于登录login 验证码captchaImage 允许匿名访问
                .antMatchers("/login", "/captchaImage").anonymous()
                .antMatchers(
                        HttpMethod.GET,
                        "/*.html",
                        "/**/*.html",
                        "/**/*.css",
                        "/**/*.js"
                ).permitAll()
                .antMatchers("/profile/**").anonymous()
                .antMatchers("/common/download**").anonymous()
                .antMatchers("/common/download/resource**").anonymous()
                .antMatchers("/swagger-ui.html").anonymous()
                .antMatchers("/swagger-resources/**").anonymous()
                .antMatchers("/webjars/**").anonymous()
                .antMatchers("/*/api-docs").anonymous()
                .antMatchers("/druid/**").anonymous()
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated()
                .and()
                .headers().frameOptions().disable();
        httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
        // 添加JWT filter
        httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        // 添加CORS filter
        httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class);
        httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class);
    }

    /**
     * 强散列哈希加密实现
     */
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder()
    {
        return new BCryptPasswordEncoder();
    }

    /**
     * 身份认证接口
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception
    {
        auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
    }
}

权限过滤JwtAuthenticationTokenFilter权限过滤器

这是一个过滤器,所有接口请求都会经过这个过滤器(包括login)

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter
{
    @Autowired
    private TokenService tokenService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException
    {
        LoginUser loginUser = tokenService.getLoginUser(request);
        if (StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication()))
        {
            tokenService.verifyToken(loginUser);
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
            authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        }
        chain.doFilter(request, response);
    }
}

getLoginUser

  1. 获取请求头中的Authorization字段的token
  2. parseToken验证token有效性,如果token不对这里会抛出异常
  3. 解析token获取token中的sessionId信息
  4. 根据sessionId获取LogInUser并返回
  /**
     * 获取用户身份信息
     * 
     * @return 用户信息
     */
    public LoginUser getLoginUser(HttpServletRequest request)
    {
        // 获取请求携带的令牌
        String token = getToken(request);
        if (StringUtils.isNotEmpty(token))
        {
            Claims claims = parseToken(token);
            // 解析对应的权限以及用户信息
            String uuid = (String) claims.get(Constants.LOGIN_USER_KEY);
            String userKey = getTokenKey(uuid);
            LoginUser user = redisCache.getCacheObject(userKey);
            return user;
        }
        return null;
    }

authenticationToken

  1. 刷新token和session
  2. 校验用户信息
  3. 更新用户信息到SpringSecurity的上下文

总结:

  1. 校验jwtToken
  2. 更新session
  3. 更新SpringSecurity上下文

用户态(session管理)

上面其实多处涉及了session,这个解决了请求时获取当前登录用户的问题。

我们可以在任意地方通过下面方法获取当前请求的用户信息,并存放的LogInUser中供业务使用

 LoginUser loginUser = tokenService.getLoginUser(request)

前端逻辑梳理

要求读者对于前端也有一定基础,可以顺着下面过程大概看一下前端的主逻辑

login.vue

填写信息点击登录时调用下面方法

  1. 参数校验
  2. 设置cookie
  3. this.$store.dispatch 使用了vuex的功能,调用store中的Login方法。这里是前端逻辑跳跃
  4. store中的user.js有Login方法,请求了后端login接口,如果成功就返回token
 handleLogin() {
      this.$refs.loginForm.validate(valid => {
        if (valid) {
          this.loading = true;
          if (this.loginForm.rememberMe) {
            Cookies.set("username", this.loginForm.username, { expires: 30 });
            Cookies.set("password", encrypt(this.loginForm.password), { expires: 30 });
            Cookies.set('rememberMe', this.loginForm.rememberMe, { expires: 30 });
          } else {
            Cookies.remove("username");
            Cookies.remove("password");
            Cookies.remove('rememberMe');
          }
          this.$store.dispatch("Login", this.loginForm).then(() => {
            this.$router.push({ path: this.redirect || "/" }).catch(()=>{});
          }).catch(() => {
            this.loading = false;
            this.getCode();
          });
        }
      });

登录接口

  1. 成功返回是获取到token
  2. 保存token到cookie
  3. 保存store中token字段全局变量
  // 登录
    Login({ commit }, userInfo) {
      const username = userInfo.username.trim()
      const password = userInfo.password
      const code = userInfo.code
      const uuid = userInfo.uuid
      return new Promise((resolve, reject) => {
        login(username, password, code, uuid).then(res => {
          let data = res.data
          setToken(data.access_token)
          commit('SET_TOKEN', data.access_token)
          setExpiresIn(data.expires_in)
          commit('SET_EXPIRES_IN', data.expires_in)
          resolve()
        }).catch(error => {
          reject(error)
        })
      })
    },

前端对于请求的封装

utils.require.js中对于所有请求拦截并添加了token

  1. 拦截请求
  2. 从cookie获取token,这里其实可以从vuex获取,但cookie有持久化效果
  3. 给所有请求添加请求头Authorization存放token,这部分和后端过滤器对应
// 创建axios实例
const service = axios.create({
  // axios中请求配置有baseURL选项,表示请求URL公共部分
  baseURL: process.env.VUE_APP_BASE_API,
  // 超时
  timeout: 10000
})

// request拦截器
service.interceptors.request.use(config => {
  // 是否需要设置 token
  const isToken = (config.headers || {}).isToken === false
  if (getToken() && !isToken) {
    config.headers['Authorization'] = 'Bearer ' + getToken() // 让每个请求携带自定义token 请根据实际情况自行修改
  }
  // get请求映射params参数
  if (config.method === 'get' && config.params) {
    let url = config.url + '?';
    for (const propName of Object.keys(config.params)) {
      const value = config.params[propName];
      var part = encodeURIComponent(propName) + "=";
      if (value !== null && typeof(value) !== "undefined") {
        if (typeof value === 'object') {
          for (const key of Object.keys(value)) {
            let params = propName + '[' + key + ']';
            var subPart = encodeURIComponent(params) + "=";
            url += subPart + encodeURIComponent(value[key]) + "&";
          }
        } else {
          url += part + encodeURIComponent(value) + "&";
        }
      }
    }
    url = url.slice(0, -1);
    config.params = {};
    config.url = url;
  }
  return config
}, error => {
    console.log(error)
    Promise.reject(error)
})

总结

以上是若依框架对于登录,token,session,鉴权等场景的处理流程。由于使用到了springSecurity让逻辑有跳跃,其实是非常简单使用的设计思路。希望对大家有帮助。

  • 16
    点赞
  • 87
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
若依框架token拦截功能是指在用户进行登录认证时,若依框架会自动生成一个token,并在后续请求中对该token进行拦截和验证。引用中提到,若依框架生成token的过程是先生成一个随机数作为redis key,然后将该随机数用于生成一个token,该token在生成后没有时效限制。当请求中包含该token时,若该token距离失效时间在10分钟以内(可配置),则若依框架会自动刷新token的有效期。 所以,若依框架token拦截功能可以帮助确保请求的有效性和安全性,防止未经授权的访问。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [05 【若依框架解读登录认证JWTtoken验证机制](https://blog.csdn.net/kouryoushine/article/details/110780053)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *3* [前后端如何实现登录token拦截校验详解](https://download.csdn.net/download/weixin_38558660/13617543)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值