首页 > 其他分享 >前后端实现双Token无感刷新用户认证

前后端实现双Token无感刷新用户认证

时间:2024-10-22 20:58:47浏览次数:1  
标签:return String accessToken 无感 Token new refreshToken 认证 public

前后端实现双Token无感刷新用户认证

本文记录了使用双Token机制实现用户认证的具体步骤,前端使用的Vue,后端使用SpringSecurity和JWT

双Token分别指的是AccessToken和RefreshToken

AccessToken:每次请求需要携带AccessToken访问后端数据,有效期短,减少AccessToken泄露带来的风险

RefreshToken:有效期长,只用于AccessToken过期时生成新的AccessToken

使用双Token机制的好处:

无感刷新:使用单个Token时,若Token过期,会强制用户重新登录,影响用户体验。双Token可以实现无感刷新,当AccessToken过期,应用会自动通过RefreshToken生成新的AccessToken,不会打断用户的操作。

提高安全性:若AccessToken有效期很长,当AccessToken被窃取后,攻击者可以长期使用这个Token,因此AccessToken的有效期不易过长。而RefreshToken只用于请求新的AccessToken和RefreshToken,它平时不会直接暴漏在网络中。

双Token认证的基本流程如下图:

1、用户登录后,服务器生成一个短期的访问令牌和一个长期的刷新令牌,并将它们发送给客户端。

2、客户端在每次请求受保护的资源时,携带访问令牌进行身份验证。

3、当访问令牌过期时,客户端使用刷新令牌向服务器请求新的访问令牌。

4、如果刷新令牌有效,服务器生成并返回新的访问令牌;否则,要求用户重新登录。

image-20241022201734278

代码实现:

本文完整代码保存在Github仓库:https://github.com/Bombtsti/DoubleTokenDemo

忽略依赖导入和配置文件,直接从代码部分开始。

首先,编写一个SpringSecurity配置类(SecurityConfig.java)进行SpringSecurity的配置。

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    //自定义JWT拦截器
    @Autowired
    private JwtLoginFilter jwtLoginFilter;
    @Autowired
    private UserDetailService userDetailService;
    //自定义认证方案
    @Autowired
    private TokenAuthenticationEntryPoint tokenAuthenticationEntryPoint;
    @Bean
    @Override
    protected AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManager();
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 关闭csrf和frameOptions,如果不关闭会影响前端请求接口(这里不展开细讲了,感兴趣的自行了解)
        http.csrf().disable();
        http.headers().frameOptions().disable();
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        // 开启跨域以便前端调用接口
        http.cors();

        // 这是配置的关键,决定哪些接口开启防护,哪些接口绕过防护
        http.authorizeRequests()
                // 注意这里,是允许前端跨域联调的一个必要配置
                .requestMatchers(CorsUtils::isPreFlightRequest).permitAll()
                // 指定某些接口不需要通过验证即可访问。登陆、注册接口肯定是不需要认证的
                .antMatchers("/api/login", "/login","/refreshToken").permitAll()
                // 这里意思是其它所有接口需要认证才能访问
                .anyRequest().authenticated();
        //http.formLogin().loginPage("/login").defaultSuccessUrl("/").permitAll();
//        http.exceptionHandling().authenticationEntryPoint(((httpServletRequest, httpServletResponse, e) -> {
//            httpServletResponse.sendRedirect("/login");
//        }));
        http.exceptionHandling().authenticationEntryPoint(tokenAuthenticationEntryPoint);
        http.addFilterBefore(jwtLoginFilter, UsernamePasswordAuthenticationFilter.class);
    }
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 指定UserDetailService和加密器
        auth.userDetailsService(userDetailService).passwordEncoder(passwordEncoder());
    }
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
    @Bean
    CorsConfigurationSource corsConfigurationSource() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowCredentials(true);
        configuration.setAllowedOrigins(Arrays.asList("*"));
        configuration.setAllowedMethods(Arrays.asList("*"));
        configuration.setAllowedHeaders(Arrays.asList("*"));
        configuration.setMaxAge(Duration.ofHours(1));
        source.registerCorsConfiguration("/**",configuration);
        return source;
    }
}

我们需要自定义一个JWT的拦截器(JwtLoginFilter.java)

@Component
public class JwtLoginFilter extends OncePerRequestFilter {

    @Autowired
    private UserDetailService userDetailService;

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        String accessToken = httpServletRequest.getHeader("accessToken");
        if(!StringUtils.hasText(accessToken)){
            filterChain.doFilter(httpServletRequest,httpServletResponse);
            return;
        }

        boolean checkToken = JWTUtil.checkToken(accessToken);
        if(!checkToken){
            throw new RuntimeException("token无效");
        }
        String username = JWTUtil.getUsername(accessToken);
        UserDetails userDetails = userDetailService.loadUserByUsername(username);
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails,null,null);
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        filterChain.doFilter(httpServletRequest,httpServletResponse);
    }
}

为了封装JWT相关的操作,可以编写了一个工具类(JWTUtil.java)

public class JWTUtil {
    //定义两个常量,1.设置过期时间 2.密钥(随机,由公司生成)
    public static final String APP_SECRET = "ukc8BDbRigUDaY6pZFfWus2jZWLPHO";

    /**
     * 生成token
     *
     * @param username
     * @param expirationTime
     * @return
     */
    public static String getJwtToken(String username, long expirationTime) {
        return Jwts.builder()
                //设置token的头信息
                .setHeaderParam("typ", "JWT")
                .setHeaderParam("alg", "HS256")
                //设置过期时间
                .setSubject("user")
                .setIssuedAt(new Date())
                //设置刷新
                .setExpiration(new Date(System.currentTimeMillis() + expirationTime))
                //设置token的主题部分
                .claim("username", username)
                //签名哈希
                .signWith(SignatureAlgorithm.HS256, APP_SECRET)
                .compact();
    }

    /**
     * 判断token是否存在与有效
     *
     * @param jwtToken
     * @return
     */
    public static boolean checkToken(String jwtToken) {
        if (StringUtils.isEmpty(jwtToken)) {
            return false;
        }
        try {
            //验证是否有效的token
            Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
        } catch (Exception e) {
            return false;
        }
        return true;
    }

    /**
     * 根据token信息得到getUserId
     *
     * @param jwtToken
     * @return
     */
    public static String getUsername(String jwtToken) {
        //验证是否有效的token
        Jws<Claims> claimsJws = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
        //得到字符串的主题部分
        Claims claims = claimsJws.getBody();
        return (String) claims.get("username");
    }

    /**
     * 判断token是否存在与有效
     *
     * @param request
     * @return
     */
    public static boolean checkToken(HttpServletRequest request) {
        try {
            String jwtToken = request.getHeader(TokenConstant.ACCESS_TOKEN);
            if (StringUtils.isEmpty(jwtToken)) {
                return false;
            }
            Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }
}

另外,在使用SpringSecurity时,我们需要编写一个UserDetail类和一个UserDetailService类分别实现UserDetails和UserDetailsService接口

@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserDetail implements UserDetails {

    @Autowired
    private User user;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;
    }

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

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

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}
@Service
public class UserDetailService implements UserDetailsService {
    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//        User user = userMapper.findByUsername(username);
        User user = new User("zlw", "$2a$10$m/4kcUo2LylsP4PKmFEFz.AcnV8DLtL/7krYxU7JcmqSPimnexd56");
        if(user==null){
            throw new UsernameNotFoundException("用户不存在");
        }else{
            return new UserDetail(user);
        }
    }
}

到这里,SpringSecurity和JWT的基本的配置完成了,接下来实现登录接口

//UserService.java
public Result<?> login(User user) {

    Authentication authenticationToken = new UsernamePasswordAuthenticationToken(user.getUsername(),user.getPassword(),null);
    Authentication authenticate = authenticationManager.authenticate(authenticationToken);
    if (Objects.isNull(authenticate)) {
        throw new RuntimeException("登陆失败");
    }

    UserDetails userDetail = userDetailService.loadUserByUsername(user.getUsername());
    //登陆并通过账号密码认证后,生成双Token返回前端
    String accessToken = JWTUtil.getJwtToken(userDetail.getUsername(), TokenConstant.ACCESS_TOKEN_EXPIRATION_TIME);
    String refreshToken = JWTUtil.getJwtToken(userDetail.getUsername(), TokenConstant.REFRESH_TOKEN_EXPIRATION_TIME);

    //把refreshToken的生成时间保存在Redis里,这是为了后面利用refreshToken生成accessToken时判断refreshToken有没有过期
    redisTemplate.opsForValue().set(userDetail.getUsername()+TokenConstant.REFRESH_TOKEN_START_TIME, String.valueOf(System.currentTimeMillis()), TokenConstant.REFRESH_TOKEN_EXPIRATION_TIME, TimeUnit.MILLISECONDS);

    Map<String,Object> map = new HashMap<>();
    map.put(TokenConstant.ACCESS_TOKEN, accessToken);
    map.put(TokenConstant.REFRESH_TOKEN, refreshToken);
    map.put("userInfo", userDetail);
    return Result.ok(map);
}

接下来,看前端的实现,写一个登录表单,在登录成功后将双Token保存在storage中。

<!--login.vue>-->
<template xmlns="http://www.w3.org/1999/html">
  <div class="loginForm">
    <div class="username">
      账号:<input placeholder="输入账号" type="text"  v-model="userLogin.username" />
    </div>
    <div class="password">
      密码:<input placeholder="输入密码" type="password"  v-model="userLogin.password"/>
    </div>
    <div class="loginBtn">
      <button @click="loginMethod">登录</button>
    </div>

    <div>
      <span>测试账号:zlw</span>
    </div>
    <div>
      <span>测试密码:123123</span>
    </div>
  </div>
</template>

<script setup>
  import {ref} from "vue";
  import {login} from "@/api/user.js";
  import {storage} from "@/utils/storage.js";
  import router from "@/router/index.js";
  import {useUserStore} from "@/store/userStore.js";

  const userStore = useUserStore();

  const userLogin = ref({
    username:"",
    password:""
  })

  const loginMethod = ()=>{
    console.log("denglu");
    login(userLogin.value).then((res)=>{
      console.log(res)
      storage.set("accessToken",res.data.accessToken);
      storage.set("refreshToken",res.data.refreshToken);
      userStore.setUserInfo(res.data.userInfo);
      console.log(res.data.accessToken);
      router.push({path:"/"});
    }).catch((error)=>{
      console.log("error");
      console.log(error);
    });
  }
</script>

其中login函数的请求方式可以单独封装到一个js文件中:

//user.js
export const login = (data)=>{
    return request({
        url:"/login",
        method:"post",
        data:data
    });
};

登录成功后,其他的请求都需要携带accessToken才能正常访问服务器的数据,我们需要配置Axios的请求拦截器和响应拦截器

//request.js
import axios from "axios";
import {useUserStore} from "@/store/userStore.js";
import {storage} from "@/utils/storage.js";

const baseURL = "http://localhost:8080/";
let isRefreshing = false;
let requestsQueue = [];

const service = axios.create({
    baseURL:baseURL,
    timeout:50000,
    headers:{"Content-Type":"application/json;charset=utf-8"}
});

//请求拦截器
service.interceptors.request.use((config)=>{
    const userStore = useUserStore();
    if(userStore.getToken){
        //请求头中加入accessToken
        config.headers.accessToken = userStore.getToken();
    }
    return config;
},(error)=>{
    return Promise.reject(error);
});


//响应拦截器
service.interceptors.response.use((res)=> {
    console.log(res);
    if (res.data.code === 200) {
        return res.data;
    }

    const config = res.config;
    //如果返回401,说明accessToken失效
    if(res.data.code===401){
        const userStore = useUserStore();
        if(!isRefreshing){
            isRefreshing = true;
            storage.set("accessToken","");
            const refreshToken = storage.get("refreshToken");
            //通过refreshToken重新请求accessToken
            return userStore.getNewToken(refreshToken).then(async (rftRes)=>{
                console.log(rftRes);
                //如果refreshToken也失效了,就重新登录
                if(rftRes.data.code===501){
                    window.location.href = "/login";
                }
                const accessToken = rftRes.data.accessToken;
                //保存新的双Token
                storage.set("accessToken",rftRes.data.accessToken);
                storage.set("refreshToken",rftRes.data.refreshToken);
                //重新发送请求
                const firstReqRes = await service.request(config);
                //执行请求队列中的请求
                requestsQueue.forEach((fuc)=>fuc(accessToken));
                requestsQueue = [];
                return firstReqRes;
            }).finally(()=>{
                isRefreshing = false;
            });
        }else{
            //并发情况下如果正在请求新token,把请求先放到一个请求队列中
            return new Promise((resolve)=>{
                requestsQueue.push((token)=>{
                    config.headers.accessToken = token;
                    resolve(service.request(config));
                });
            });
        }
    }
    return Promise.reject(res);

},(error)=>{
    console.log("登陆失败");
    window.localStorage.clear();
    window.location.href = "/login";
});
export default service;

在响应拦截器中,当返回状态码401,说明accessToken已经过期了,这时需要从store中拿到refreshToken,并用refreshToken重新请求新的双Token,后端的实现接口如下:

//UserService.java
public Result<?> refreshToken(String refreshToken) {
    Map<String,Object> map = new HashMap<>();
    String username = JWTUtil.getUsername(refreshToken);
    String accessToken = JWTUtil.getJwtToken(username,TokenConstant.ACCESS_TOKEN_EXPIRATION_TIME);

    String refreshTokenStr = (String) redisTemplate.opsForValue().get(username+TokenConstant.REFRESH_TOKEN_START_TIME);
    if(StringUtils.isBlank(refreshTokenStr)){
        return Result.fail(map);
    }
    long refreshTokenStartTime = Long.parseLong(refreshTokenStr);
	//如果refreshToken也过期了,就返回501错误码
    if(refreshTokenStartTime+TokenConstant.REFRESH_TOKEN_EXPIRATION_TIME < System.currentTimeMillis()){
        return Result.forbidden(map);
    } else if(refreshTokenStartTime+TokenConstant.REFRESH_TOKEN_EXPIRATION_TIME-System.currentTimeMillis()<=TokenConstant.ACCESS_TOKEN_EXPIRATION_TIME){
        //如果refreshToken快过期了,就生成一个新的refreshToken
        refreshToken = JWTUtil.getJwtToken(username,TokenConstant.REFRESH_TOKEN_EXPIRATION_TIME);
        redisTemplate.opsForValue().set(username+TokenConstant.REFRESH_TOKEN_START_TIME , String.valueOf(System.currentTimeMillis()), TokenConstant.REFRESH_TOKEN_EXPIRATION_TIME, TimeUnit.MILLISECONDS);
    }

    map.put(TokenConstant.ACCESS_TOKEN,accessToken);
    map.put(TokenConstant.REFRESH_TOKEN,refreshToken);
    return Result.ok(map);
}

更具体的代码保存在Github仓库中:https://github.com/Bombtsti/DoubleTokenDemo

标签:return,String,accessToken,无感,Token,new,refreshToken,认证,public
From: https://www.cnblogs.com/Linwei33/p/18493726

相关文章

  • 【数字化转型到底转了啥?】学习华为HCIP认证后谈谈华为的数字化转型
     背景: 最近正在学习华为HCIP认证相关课程,其中第一讲就是关于企业架构和数字化转型的课程。谈一谈总结和感想,华为的数字化转型,真的就像是一次华丽的蜕变,他们通过数字化转型,把业务都重新梳理了一遍,让效率变得超级高,客户体验也变得超级棒。这种变革,真的让人感受到了数字化转型......
  • 对CSP-S认证知识面的分析
    CCF举办的CSP-S认证从2019年开始,在这几年间,复赛的题目类型各有不同。分析一些客观的过去数据题目难度使用Luogu的题目评级机制,在过去的几年中:难度数量普及-\(2\)普及/提高−\(1\)普及+/提高\(5\)提高+/省选−\(7\)省选/NOI−\(5\)NOI/NOI......
  • 必看!如何获得免费的Salesforce AI认证?
    为了帮助更多人学习和掌握AI工具,Salesforce决定投入5000万美元,推出各种提升技能的计划,包含实践workshop,讲师授课,以及免费的AI认证考试。01线上hands-onworkshop从2024年11月起,Salesforce计划推出线上workshop,内容如下:如何创建你的第一个Agent使用Agentforce管理销售团队......
  • java脚手架系列9-统一权限认证gateway
    之所以想写这一系列,是因为之前工作过程中有几次项目是从零开始搭建的,而且项目涉及的内容还不少。在这过程中,遇到了很多棘手的非业务问题,在不断实践过程中慢慢积累出一些基本的实践经验,认为这些与业务无关的基本的实践经验其实可以复刻到其它项目上,在行业内可能称为脚手架,因......
  • ASP.NET Cookie身份认证
    1.添加Cookie身份验证方案services.AddAuthentication(option=>{option.DefaultAuthenticateScheme=CookieAuthenticationDefaults.AuthenticationScheme;option.DefaultChallengeScheme=CookieAuthenticationDefaults.AuthenticationScheme;}).AddCookie(Coo......
  • 【论文阅读笔记】An Image is Worth 1/2 Tokens After Layer 2: Plug-and-Play Infere
    论文地址:https://arxiv.org/pdf/2403.06764代码地址:https://github.com/pkunlp-icler/FastV目录IntroductionInefficientVisualAttentioninVLLMsPreliminaries两种分数结果分析FastVOverviewRe-rankandFilteringmodule(core)ThoughtIntroduction现象(问题):大多数LVL......
  • 登录功能-Java实现token的生成与验证
    一、token与cookie相比较的优势1、支持跨域访问,将token置于请求头中,而cookie是不支持跨域访问的;2、无状态化,服务端无需存储token,只需要验证token信息是否正确即可,而session需要在服务端存储,一般是通过cookie中的sessionID在服务端查找对应的session;3、无需绑定到一个特殊的身份......
  • TMtech凯钰T8332AD升降压LED驱动芯片AEC-Q100认证
    T8332AD是TMTechnology,Inc.设计的一款多功能LED驱动IC。它具有广泛的输入电压范围、精确的恒流控制和多种保护机制,非常适合各种大功率LED应用。以下是其主要特点、应用和技术规格的概述。主要特点1.宽输入电压范围:在5V到60V之间高效运行。2.精确的电流控制......
  • Flask中如何实现JWT认证?
    在Flask中实现JWT(JSONWebToken)认证,通常需要借助第三方库,比如PyJWT或Flask-JWT-Extended。下面我会分别介绍如何使用这两个库来实现JWT认证。使用PyJWT安装PyJWT首先,你需要安装PyJWT库。可以使用pip来安装:pipinstallPyJWT生成JWT在Flask应用中,你可以创建一个函数......
  • Django drf jwt token认证前后端使用流程
    在DjangoRestFramework(DRF)中使用JWT(JSONWebToken)进行认证时,前后端需要配合工作。下面是DRF使用JWT认证的一个基本流程。后端部分安装必要的库:需要安装djangorestframework和djangorestframework-simplejwt两个库。后者是处理JWT的工具。pipin......