springboot集成JWT之双重token

一,单个token缺点

token一般存储在浏览器中,容易被盗取,而为了防止被盗取长期使用,token的有效时间必然无法不能设置太长,此举也必然引起用户的频繁登录,给用户带来不好的体验

二,双重token(accessToken,refreshToken)

(一)设计思路

1,将accessToken作为是否登录的标识,存储在浏览器当中。由于该token可浏览器看到,容易被盗取,可将有效时间设置的尽可能短一些,解决盗取长期使用问题
2,将refreshToken作为是否更新accessToken的标识,只要refreshToken不过期,则自动更新accessToken,可将有效时间设置的长些,解决频繁登录问题
3,不直接将refreshToken存储到浏览器上,而是将其存储到accessToken的载荷里,后端通过获取accessToken的载荷内容获取到refreshToken,防止refreshToken被盗取
4,accessToken构成:
         载荷:refreshToken
         有效时间:尽可能短(这里我设置为30分钟)
         密钥:用户的密码
     refreshToken构成:
         载荷:用户的id
         有效时间:长(这里我设置为一天)
         密钥:用户的密码

(二)后端代码

1,TokenUtil(生成token和获取用户信息)

具体步骤看注释

@Component
@Slf4j
public class TokenUtils {
    private static IUserService staticAdminService;

    @Resource
    private IUserService adminService;

    @PostConstruct
    public void setUserService() {
        staticAdminService = adminService;
    }
    
	//	生成token
    public static String genToken(String adminId, String sign, Integer time) {
    	//生成过期时间
        Calendar instance = Calendar.getInstance();
        instance.add(Calendar.SECOND, time*60);

        return JWT.create().withAudience(adminId) //载荷
                .withExpiresAt(instance.getTime()) //time分钟后过期
                .sign(Algorithm.HMAC256(sign)); // 密钥
    }
    
    //根据accessToken获取用户信息
    //  1.通过accessToken的载荷拿到refreshToken
    //  2.通过refreshToken的载荷拿到userId
    //  3.调用根据用户id获取用户信息的方法拿到用户信息
    public static User getCurrentAdmin() {
        String accessToken = null;
        try {
            HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
            accessToken = request.getHeader("token");
            System.out.println("access"+accessToken);
            if (StrUtil.isBlank(accessToken)) {
                log.error("获取当前登录的accessToken失败, token: {}", accessToken);
                return null;
            }
            String refreshToken = JWT.decode(accessToken).getAudience().get(0);
            if (StrUtil.isBlank(refreshToken)) {
                log.error("获取当前登录的refreshToken失败, token: {}", refreshToken);
                return null;
            }
            String userId = JWT.decode(refreshToken).getAudience().get(0);
            return staticAdminService.getById(Integer.valueOf(userId));
        } catch (Exception e) {
            log.error("获取当前登录的管理员信息失败, token={}", accessToken,  e);
            return null;
        }
    }
}

2,JwtInterceptor(检验accessToken是否合法)

实现思路:
(1)检验accessToken是否为空
(2)通过accessToken的载荷内容拿到refreshToken
(3)验证获取到的refreshToken是否合法,若不合法,则accessToken为无效token,合法则返回密钥
(4)通过该密钥去判断accessToken是否过期

@Slf4j
public class JwtInterceptor implements HandlerInterceptor {

    @Autowired
    private IUserService userService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String accessToken = request.getHeader("token");
        if (StrUtil.isBlank(accessToken)) {
            accessToken = request.getParameter("token");
        }
        //执行认证
        if (StrUtil.isBlank(accessToken)) {
            throw new ServiceException(ErrorCode.TOKEN_NO_EXIST.getCode(), ErrorCode.TOKEN_NO_EXIST.getMsg());
        }
        String refreshToken;
        try{
            refreshToken = JWT.decode(accessToken).getAudience().get(0);
        }catch (Exception e){
            throw new ServiceException(ErrorCode.TOKEN_ERROR.getCode(), ErrorCode.TOKEN_ERROR.getMsg());
        }
        String password = verifyRefreshToken(refreshToken);
        try {
            // 用户密码加签验证 token
            JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(password)).build();
            jwtVerifier.verify(accessToken); // 验证token
        } catch (JWTVerificationException e) {
            throw new ServiceException(ErrorCode.TOKEN_ERROR.getCode(), ErrorCode.TOKEN_ERROR.getMsg());
        }
        return true;
    }
	
	//验证refreshToken是否合法
    public String verifyRefreshToken(String token){
        // 获取 token 中的adminId
        String adminId;
        User user;
        try {
            adminId = JWT.decode(token).getAudience().get(0);
            // 根据token中的userid查询数据库
            user = userService.getById(Integer.parseInt(adminId));
            System.out.println(user);
        } catch (Exception e) {
            e.printStackTrace();
            throw new ServiceException(ErrorCode.REFRESE_TOKEN_ERROR.getCode(), ErrorCode.REFRESE_TOKEN_ERROR.getMsg());
        }
        if (user == null) {
            throw new ServiceException(ErrorCode.USER_NO_EXIST.getCode(), ErrorCode.USER_NO_EXIST.getMsg());
        }

        try {
            // 用户密码加签验证 token
            JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(user.getPassword())).build();
            jwtVerifier.verify(token); // 验证token
        } catch (JWTVerificationException e) {
            throw new ServiceException(ErrorCode.REFRESE_TOKEN_ERROR.getCode(), ErrorCode.REFRESE_TOKEN_ERROR.getMsg());
        }
        return user.getPassword();
    }
}

3, WebConfig(设置拦截规则)

@Configuration
public class WebConfig implements  WebMvcConfigurer {
    // 加自定义拦截器JwtInterceptor,设置拦截规则
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(jwtInterceptor()).addPathPatterns("/**").excludePathPatterns("/user/login","/user/register","/refreshToken/refresh");
    }
    @Bean
    public JwtInterceptor jwtInterceptor(){
        return new JwtInterceptor();
    }
}

4,登陆接口处生成accessToken和refreshToken

String refreshToken= TokenUtils.genToken(user.getId().toString(),user.getPassword(),24*60);
String accessToken=TokenUtils.genToken(refreshToken,user.getPassword(),30);

5,编写更新refreshToken的接口

实现思路:
(1)检验accessToken是否为空
(2)通过accessToken的载荷内容拿到refreshToken
(3)验证获取到的refreshToken是否合法,不合法,则accessToken为无效token(此处无效token单指自己编写,不是后端生成的token),不对accessToken进行更新操作,合法则重新生成新的accessToken

@RestController
@RequestMapping("/refreshToken")
public class refreshTokenController {
    @Autowired
    private IUserService userService;

    @GetMapping("/refresh")
    public Result refresh(@RequestParam String accessToken) {
        if (StrUtil.isBlank(accessToken)) {
            throw new ServiceException(ErrorCode.TOKEN_NO_EXIST.getCode(), ErrorCode.TOKEN_NO_EXIST.getMsg());
        }
        String refreshToken;
        try{
            refreshToken = JWT.decode(accessToken).getAudience().get(0);
        }catch (Exception e){
            throw new ServiceException(ErrorCode.TOKEN_ERROR.getCode(), ErrorCode.TOKEN_ERROR.getMsg());
        }
        if(StrUtil.isBlank(refreshToken)){
            return Result.error(ErrorCode.REFRESH_TOKEN_NULL.getCode(), ErrorCode.REFRESH_TOKEN_NULL.getMsg());
        }
        // 获取 token 中的adminId
        String adminId;
        User user;
        try {
            adminId = JWT.decode(refreshToken).getAudience().get(0);
            // 根据token中的userid查询数据库
            user = userService.getById(Integer.parseInt(adminId));
            System.out.println(user);
        } catch (Exception e) {
            e.printStackTrace();
            throw new ServiceException(ErrorCode.REFRESE_TOKEN_ERROR.getCode(), ErrorCode.REFRESE_TOKEN_ERROR.getMsg());
        }
        if (user == null) {
            throw new ServiceException(ErrorCode.USER_NO_EXIST.getCode(), ErrorCode.USER_NO_EXIST.getMsg());
        }

        try {
            // 用户密码加签验证 token
            JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(user.getPassword())).build();
            jwtVerifier.verify(refreshToken); // 验证token
        } catch (JWTVerificationException e) {
            throw new ServiceException(ErrorCode.REFRESE_TOKEN_ERROR.getCode(), ErrorCode.REFRESE_TOKEN_ERROR.getMsg());
        }
        String currentAccessToken= TokenUtils.genToken(refreshToken,user.getPassword(),30);
        return Result.success(currentAccessToken);
    }
}

(三)前端代码

1,登录成功后存储accessToken

存储方式不限,在这我是存储在localStorage里,并在vuex里共享该数据

//storage.js
const TOKEN_KEY = 'tk'
export const getInfo = () => {
  const token = localStorage.getItem(TOKEN_KEY)
  return token || ''
}
export const setInfo = (token) => {
  localStorage.setItem(TOKEN_KEY, token)
}
export const removeInfo = () => {
  localStorage.removeItem(TOKEN_KEY)
}

//token模块
import { getInfo, setInfo ,removeInfo} from '@/utils/storage'
export default {
  namespaced: true,
  state () {
    return {
      tokenInfo: getInfo()
    }
  },
  mutations: {
    setTokenInfo (state, info) {
      state.tokenInfo = info
      setInfo(state.tokenInfo)
    },
    removeTokenInfo () {
      removeInfo()
    }
  },
  actions: {}
}
store.commit('token/setTokenInfo', data)

2,axios请求拦截器处设置请求头

instance.interceptors.request.use(function (config) {
  const accessToken = store.state.token.tokenInfo
  if (accessToken) {
    config.headers.token = accessToken
  }
  return config
}, function (error) {
  // 对请求错误做些什么
  return Promise.reject(error)
})

3,axios响应拦截器配置

设置accessToken过期则判断refreshToken是否过期,如果未过期,重新生成accessToken,并重新发送请求

instance.interceptors.response.use(async function (response) {
  if (response.data.code === 200) {
    return response.data
  } else if (response.data.code === 1008 || response.data.code === 1009 || response.data.code === 1010) {
  	//accessToken过期
    const { data } = await refreshToken(store.state.token.tokenInfo)
    store.commit('token/setTokenInfo', data)
    return instance(response.config)
  } else if (response.data.code === 1014) {
  	//refreshToken过期
    window.location.href = '/login'
  } else {
    Vue.prototype.$message.error(response.data.msg)
    return Promise.reject(response.data.msg)
  }
}, function (error) {
  // 对响应错误做点什么
  return Promise.reject(error)
})

4,守卫路由配置

判断是否有accessToken,是否在登录注册页面,若都无则跳转到登录页,重新登录

router.beforeEach(async (to, from, next) => {
  const token = store.state.token.tokenInfo
  if (
    // 检查用户是否已登录
    !token &&
    // ❗️ 避免无限重定向
    to.path !== '/login' && to.path !== '/register'
  ) {
    // 将用户重定向到登录页面
    next({ path: '/login' })
  } else {
    next()
  }
})
### 回答1: Spring Boot是一个开源的Java框架,用于快速开发基于Spring的应用程序。它提供了许多功能和工具,其中包括支持WebSocket的功能。 WebSocket是一种用于实现向通信的协议,在Web应用程序中可以用于实时通信和数据推送。在Spring Boot中使用WebSocket可以轻松地实现实时通信功能。 要实现基于token的身份认证,可以按照以下步骤进行: 1. 创建一个WebSocket处理程序:在Spring Boot中,可以使用@ServerEndpoint注解创建一个WebSocket处理程序。在该处理程序中,可以定义onOpen、onMessage、onClose和onError等方法来处理WebSocket连接的生命周期事件。 2. 创建一个Token认证过滤器:可以使用Spring Security框架来实现基于token的身份认证。创建一个Token认证过滤器,将其配置为在WebSocket连接建立之前进行身份认证。 3. 在WebSocket处理程序中验证token:在WebSocket处理程序的onOpen方法中,可以获取到WebSocket连接的会话对象。可以使用这个会话对象来获取到发送的token,并将其验证。 4. 发送认证结果:根据token的验证结果,可以发送不同的消息给客户端。如果验证成功,则可以发送连接成功的消息给客户端;如果验证失败,则可以发送连接失败的消息给客户端。 通过以上步骤,就可以实现基于token的身份认证了。客户端在建立WebSocket连接时,需要将token作为参数发送给服务器。服务器在接收到连接请求后,会进行身份认证,根据认证结果发送相应的消息给客户端。 使用Spring Boot和WebSocket实现基于token的身份认证,可以让应用程序更安全和可靠。同时,使用Spring Security可以提供更多的身份认证和授权功能,进一步增强应用程序的安全性。 ### 回答2: Spring Boot提供了一个强大的WebSocket支持,可以非常方便地实现Token身份认证。 要实现WebSocket的Token身份认证,首先需要创建一个WebSocket处理程序,可以通过实现`WebSocketHandler`接口或者继承`TextWebSocketHandler`类来实现。然后,可以使用`@Component`注解将该处理程序注册为Spring组件。 接下来,我们需要对WebSocket进行配置,可以创建一个类继承自`WebSocketConfigurer`接口,并实现其中的`registerWebSocketHandlers`方法。在这个方法中,我们可以指定处理程序的路径,并添加自定义的拦截器,用于Token的身份认证。 在拦截器中,可以通过WebSocket握手时的`HandshakeInterceptor`,在`afterHandshake`方法中进行身份认证逻辑的处理。可以通过获取WebSocket握手的`HttpServletRequest`和`HttpServletResponse`,来获取和验证Token。如果Token验证通过,则可以继续进行握手,并返回true;否则,可以拒绝握手,返回false。 在身份认证通过后,可以通过`WebSocketSession`发送和接收消息。在发送消息时,可以通过`sendMessage`方法发送消息给指定的WebSocket会话;在接收消息时,可以通过实现`WebSocketHandler`接口的`handleTextMessage`方法来处理接收到的消息。 总结起来,要实现Spring Boot WebSocket的Token身份认证,需要创建WebSocket处理程序,配置WebSocket,添加拦截器进行Token验证,并在处理程序中处理收发消息的逻辑。这样,我们就可以在Spring Boot中实现带有Token身份认证的WebSocket功能了。 ### 回答3: Spring Boot提供了一种简便的方式来实现WebSocket身份认证,可以使用Token来验证用户身份。下面是一个简单的实现步骤。 首先,需要在Spring Boot项目中配置WebSocket,并添加相关依赖。可以通过在pom.xml文件中添加以下依赖来引入WebSocket支持: ``` <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> ``` 接下来,创建一个WebSocket配置类,继承自`AbstractWebSocketMessageBrokerConfigurer`类,并覆盖其中的方法。在`registerStompEndpoints()`方法中,可以设置WebSocket的端点和消息传输方式: ```java @Configuration @EnableWebSocketMessageBroker public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer { @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/websocket").withSockJS(); } @Override public void configureMessageBroker(MessageBrokerRegistry registry) { registry.enableSimpleBroker("/topic"); registry.setApplicationDestinationPrefixes("/app"); } } ``` 然后,在WebSocket处理类中,可以实现`WebSocketConfigurer`接口,通过重写其中的方法来进行身份验证。在`registerWebSocketHandlers()`方法中,可以设置拦截器来验证Token: ```java @Configuration public class WebSocketHandlerConfig implements WebSocketConfigurer { @Autowired private WebSocketInterceptor webSocketInterceptor; @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(myHandler(), "/websocket") .addInterceptors(webSocketInterceptor) .setAllowedOrigins("*"); } @Bean public WebSocketHandler myHandler() { return new MyHandler(); } } ``` 在拦截器`WebSocketInterceptor`中,可以在用户连接WebSocket之前验证Token的有效性,例如检查Token是否过期、用户是否存在等: ```java @Component public class WebSocketInterceptor implements HandshakeInterceptor { @Override public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception { // 根据Token验证用户身份 // 如果验证失败,可以使用response返回错误信息,然后返回false拒绝连接 // 如果验证成功,可以在attributes中存储用户信息,以便后面使用 return true; } @Override public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) { } } ``` 最后,在处理WebSocket消息的类中,可以通过获取用户信息来实现对特定用户的消息推送等操作: ```java @Component public class MyHandler extends TextWebSocketHandler { @Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { // 根据用户信息保存WebSocketSession等操作 } @Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { // 处理用户传输的消息 } } ``` 通过以上方式,可以实现在Spring Boot中使用Token进行WebSocket身份认证。根据具体的需求,可以定制化处理用户信息、Token验证等功能。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值