录,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);

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

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);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

以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);
    }

 
 
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

生成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
  • 4
  • 5
  • 6
  • 7

总结:

  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());
}

}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127

权限过滤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) &amp;&amp; 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);
}

}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

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;
    }

 
 
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

authenticationToken

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

总结:

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

用户态(session管理)

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

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

 LoginUser loginUser = tokenService.getLoginUser(request)

 
 
 
 
  • 1

前端逻辑梳理

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

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
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

登录接口

  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)
        })
      })
    },

 
 
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

前端对于请求的封装

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)
})

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42

总结

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值