深入浅出Bearer Token:解析工作原理及其在Vue、Uni-app与Java中的实现Demo

目录

  • 前言
  • [1. 基本知识](#1. 基本知识)
  • [2. Demo](#2. Demo)
  • [3. 实战](#3. 实战)

前言

🤟 找工作,来万码优才:👉 #小程序://万码优才/r6rqmzDaXpYkJZF

1. 基本知识

Bearer Token是一种基于Token的认证机制,用于在HTTP请求中传递用户的身份信息

应用于RESTful API和各种Web应用中,提供了一种轻量且高效的身份验证方式

基本的作用如下:

  • 身份验证:通过Token验证用户的身份,确定其是否有权访问某个资源
  • 授权:Token中可以包含用户的权限信息,服务器可以根据Token中的信息决定用户可以进行的操作
  • 无状态认证:服务器不需要保存用户的会话状态,只需解析Token即可验证用户身份,使系统更易于扩展和管理

具体的工作原理如下:

  1. 用户登录:用户向服务器发送登录请求,提供用户名和密码等认证信息
  2. Token生成:服务器验证用户信息后,生成一个包含用户身份和权限信息的Bearer Token。 Token可以是纯字符串,也可以是经过签名的JWT(JSON Web Token)
  3. Token传输:服务器将生成的Token返回给客户端,客户端将其存储在本地(如浏览器的Local Storage或Cookie中)
  4. 请求资源:客户端在后续的HTTP请求中,将Bearer Token放在Authorization头中,发送到服务器
  5. Token验证:服务器接收到请求后,提取Bearer Token,解析其内容以验证用户身份和权限,决定是否允许请求

融入个人的一点小思考:

Uni-app部分,由于它是跨平台的框架,语法和Vue类似,我可以使用类似的逻辑来实现登录和.Token的管理。只需确保在不同设备上的本地存储方式一致即可。

在后端,使用Java开发时,我需要编写一个控制器来处理登录请求,生成Token,并将其返回给前端。同时,还需要编写过滤器或拦截器,用于检查每个请求的Authorization头,解析Token并验证。如果Token有效,则允许请求继续;否则,拒绝访问。

那么,如何辨别前端传来的Token是否有效呢?在后端,可以利用JWT的特性,比如检查Token的签名是否正确,Token是否过期,以及Token中的用户信息是否合法。也可以实现Token的黑名单机制,支持用户注销或停止某些Token的使用。

在实际项目中,还需要考虑Token的安全性,比如防止CSRF攻击、存储Token的方式(本地存储、Cookie等)、Token的有效期等。此外,还需要应对Token被泄露或被截获的风险,建议在传输中使用HTTPS协议。

经过以上的思考,我对Bearer Token有了基本的理解。它是一种基于Token的认证方式,适用于分布式系统和无状态的API设计。通过Token的生成、存储和验证,可以在没有会话管理的情况下实现用户的身份认证

~ 具体JWT的流程如下:

前端发送登录请求,包含用户名和密码

后端验证用户信息,创建Header和Payload

使用保密密钥对Header和Payload进行签名,生成完整的JWT

将JWT返回给前端,前端存储它

~ jwt验证流程:

前端在每次请求中携带JWT

后端提取JWT,验证其签名,确保未被篡改

解析Payload中的用户信息,进行权限检查

2. Demo

接下来会以Vue 以及 Uniapp对接Java的方式呈现一个Demo,主要是提供一个思路

实现Bearer Token的步骤

  1. 用户登录:客户端发送登录请求,携带用户名和密码;服务器验证用户信息,成功后生成Bearer Token;返回Token给客户端
  2. 存储Token:客户端将Token存储在安全的位置,如HTTP-only Cookie或Local Storage中;确保Token不会被恶意脚本窃取,推荐使用HTTPS传输
  3. 发送请求:客户端在后续请求中,在Authorization头添加"Bearer ";服务器接收到请求后,解析Token,验证用户身份
  4. Token验证:服务器检查Token的签名是否有效,确保Token未被篡改;验证Token是否过期,获取用户信息和权限;授权或拒绝请求

Bearer Token的过期与刷新

过期时间定义Token的存活时间,通常几分钟到几小时不等。Token过期后,用户需要重新登录以获取新的Token

刷新Token:用户在Token过期前,可以请求刷新Token,获取新的有效Token

  1. Vue实现

登录组件

html 复制代码
<template>
  <div class="login">
    <h2>登录</h2>
    <form @submit.prevent="handleLogin">
      <div class="form-group">
        <label for="username">用户名</label>
        <input type="text" id="username" v-model="username" required>
      </div>
      <div class="form-group">
        <label for="password">密码</label>
        <input type="password" id="password" v-model="password" required>
      </div>
      <button type="submit">登录</button>
    </form>
  </div>
</template>

<script>
export default {
  data() {
    return {
      username: '',
      password: ''
    };
  },
  methods: {
    async handleLogin() {
      try {
        const response = await this.$axios.post('/api/login', {
          username: this.username,
          password: this.password
        });
        
        const token = response.data.token;
        localStorage.setItem('authToken', token);
        this.$router.push('/dashboard');
      } catch (error) {
        console.error('登录失败:', error);
      }
    }
  }
};
</script>

请求拦截器

js 复制代码
// main.js
import Vue from 'vue';
import axios from 'axios';

axios.interceptors.request.use(config => {
  const token = localStorage.getItem('authToken');
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

Vue.config.productionTip = false;
new Vue({
  render: h => h(App),
}).$mount('#app');
  1. Uni-app实现

登录页面

html 复制代码
<template>
  <view class="login">
    <h2>登录</h2>
    <form @submit="handleLogin">
      <view class="form-group">
        <label>用户名</label>
        <input type="text" v-model="username" />
      </view>
      <view class="form-group">
        <label>密码</label>
        <input type="password" v-model="password" />
      </view>
      <button form-type="submit">登录</button>
    </form>
  </view>
</template>

<script>
export default {
  data() {
    return {
      username: '',
      password: ''
    };
  },
  methods: {
    async handleLogin() {
      try {
        const response = await this.$axios.post('/api/login', {
          username: this.username,
          password: this.password
        });
        
        const token = response.data.token;
        uni.setStorageSync('authToken', token);
        uni.navigateTo({ url: '/pages/dashboard/dashboard' });
      } catch (error) {
        console.error('登录失败:', error);
      }
    }
  }
};
</script>

Uni-app 请求拦截器

js 复制代码
// app.vue
import Vue from 'vue';
import axios from 'axios';

axios.interceptors.request.use(config => {
  const token = uni.getStorageSync('authToken');
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

Vue.config.productionTip = false;

App.mpType = 'app';

const app = new Vue({
  ...App
});

app.$mount();

四、后端实现:Java

User实体类

java 复制代码
@AllArgsConstructor
@NoArgsConstructor
@Data
@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String username;

    private String password;

    @Enumerated(EnumType.STRING)
    private Role role;
}

Role枚举

java 复制代码
public enum Role {
    USER, ADMIN
}

JwtConfig配置

java 复制代码
@Configuration
public class JwtConfig {
    @Value("${ jwt.secret}")
    private String secret;

    @Value("${ jwt.expiration}")
    private Long expiration;

    @Value("${ jwt.header}")
    private String header;

    public String getSecret() {
        return secret;
    }

    public Long getExpiration() {
        return expiration;
    }

    public String getHeader() {
        return header;
    }
}

JwtUtil工具类

java 复制代码
@Component
public class JwtUtil {
    @Autowired
    private JwtConfig config;

    public String generateToken(User user) {
        Map<String, Object> claims = new HashMap<>();
        claims.put("id", user.getId());
        claims.put("username", user.getUsername());
        claims.put("role", user.getRole());
        claims.put("iat", new Date());
        claims.put("exp", new Date(System.currentTimeMillis() + config.getExpiration()));

        return Jwts.builder()
                .setClaims(claims)
                .signWith(SignatureAlgorithm.HS256, config.getSecret().getBytes())
                .compact();
    }

    public boolean validateToken(String token) {
        try {
            Jwts.parser().setSigningKey(config.getSecret().getBytes()).parseClaimsJws(token);
            return true;
        } catch (Exception e) {
            return false;
        }
    }

    public Claims getTokenBody(String token) {
        return Jwts.parser().setSigningKey(config.getSecret().getBytes()).parseClaimsJws(token).getBody();
    }
}

LoginController

java 复制代码
@RestController
@RequestMapping("/api")
public class LoginController {
    @Autowired
    private JwtUtil jwtUtil;

    @Autowired
    private UserRepository userRepository;

    @PostMapping("/login")
    public ResponseEntity<?> login(@RequestBody LoginRequest loginRequest) {
        User user = userRepository.findByUsername(loginRequest.getUsername())
                .orElseThrow(() -> new BadCredentialsException("用户不存在"));

        if (!user.getPassword().equals	loginRequest.getPassword()) {
            throw new BadCredentialsException("密码错误");
        }

        String token = jwtUtil.generateToken(user);
        return ResponseEntity.ok(new ApiResponse("登录成功", token));
    }
}

SecurityConfig

java 复制代码
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
            .authorizeRequests()
            .antMatchers("/api/login").permitAll()
            .anyRequest().authenticated()
            .and()
            .addFilterBefore(new JwtAuthenticationFilter(), BasicAuthenticationFilter.class);
    }
}

JwtAuthenticationFilter

java 复制代码
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    @Autowired
    private JwtUtil jwtUtil;

    @Override
    protected void doFilterInternal(HttpServletRequest request, 
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {
        String header = request.getHeader("Authorization");
        if (header != null && header.startsWith("Bearer ")) {
            String token = header.substring(7);
            if (jwtUtil.validateToken(token)) {
                Claims claims = jwtUtil.getTokenBody(token);
                UsernamePasswordAuthenticationToken authentication = new 
                    UsernamePasswordAuthenticationToken(
                        claims.getSubject(), null, 
                        getAuthorities((List<String>) claims.get("role"))
                    );
                SecurityContextHolder.getContext().setAuthentication(authentication);
            } else {
                response.setStatus(HttpServletResponse.SC_FORBIDDEN);
                return;
            }
        }
        filterChain.doFilter(request, response);
    }

    private Collection<? extends GrantedAuthority> getAuthorities(List<String> roles) {
        return roles.stream()
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());
    }
}

3. 实战

实战中的Demo测试如下:

以下代码用于实战中的讲解,代码来源:https://gitee.com/zhijiantianya/ruoyi-vue-pro

uniapp中封装独特的request请求:

js 复制代码
import store from '@/store'
import config from '@/config'
import { getAccessToken } from '@/utils/auth'
import errorCode from '@/utils/errorCode'
import { toast, showConfirm, tansParams } from '@/utils/common'

let timeout = 10000
const baseUrl = config.baseUrl + config.baseApi;

const request = config => {
  // 是否需要设置 token
  const isToken = (config.headers || {}).isToken === false
  config.header = config.header || {}
  if (getAccessToken() && !isToken) {
    config.header['Authorization'] = 'Bearer ' + getAccessToken()
  }
  // 设置租户 TODO 芋艿:强制 1 先
  config.header['tenant-id'] = '1';
  // get请求映射params参数
  if (config.params) {
    let url = config.url + '?' + tansParams(config.params)
    url = url.slice(0, -1)
    config.url = url
  }
  return new Promise((resolve, reject) => {
    uni.request({
        method: config.method || 'get',
        timeout: config.timeout ||  timeout,
        url: config.baseUrl || baseUrl + config.url,
        data: config.data,
        // header: config.header,
        header: config.header,
        dataType: 'json'
      }).then(response => {
        let [error, res] = response
        if (error) {
          toast('后端接口连接异常')
          reject('后端接口连接异常')
          return
        }
        const code = res.data.code || 200
        const msg = errorCode[code] || res.data.msg || errorCode['default']
        if (code === 401) {
          showConfirm('登录状态已过期,您可以继续留在该页面,或者重新登录?').then(res => {
            if (res.confirm) {
              store.dispatch('LogOut').then(res => {
                uni.reLaunch({ url: '/pages/login' })
              })
            }
          })
          reject('无效的会话,或者会话已过期,请重新登录。')
        } else if (code === 500) {
          toast(msg)
          reject('500')
        } else if (code !== 200) {
          toast(msg)
          reject(code)
        }
        resolve(res.data)
      })
      .catch(error => {
        let { message } = error
        if (message === 'Network Error') {
          message = '后端接口连接异常'
        } else if (message.includes('timeout')) {
          message = '系统接口请求超时'
        } else if (message.includes('Request failed with status code')) {
          message = '系统接口' + message.substr(message.length - 3) + '异常'
        }
        toast(message)
        reject(error)
      })
  })
}

export default request

其中token都是存放本地:

js 复制代码
const AccessTokenKey = 'ACCESS_TOKEN'
const RefreshTokenKey = 'REFRESH_TOKEN'

// ========== Token 相关 ==========

export function getAccessToken() {
  return uni.getStorageSync(AccessTokenKey)
}

export function getRefreshToken() {
  return uni.getStorageSync(RefreshTokenKey)
}

export function setToken(token) {
  uni.setStorageSync(AccessTokenKey, token.accessToken)
  uni.setStorageSync(RefreshTokenKey, token.refreshToken)
}

export function removeToken() {
  uni.removeStorageSync(AccessTokenKey)
  uni.removeStorageSync(RefreshTokenKey)
}

后续只需要发送给后端即可

再说说前段也同理:

制作一个Jwt的格式:

js 复制代码
import { useCache, CACHE_KEY } from '@/hooks/web/useCache'
import { TokenType } from '@/api/login/types'
import { decrypt, encrypt } from '@/utils/jsencrypt'

const { wsCache } = useCache()

const AccessTokenKey = 'ACCESS_TOKEN'
const RefreshTokenKey = 'REFRESH_TOKEN'

// 获取token
export const getAccessToken = () => {
  // 此处与TokenKey相同,此写法解决初始化时Cookies中不存在TokenKey报错
  return wsCache.get(AccessTokenKey) ? wsCache.get(AccessTokenKey) : wsCache.get('ACCESS_TOKEN')
}

// 刷新token
export const getRefreshToken = () => {
  return wsCache.get(RefreshTokenKey)
}

// 设置token
export const setToken = (token: TokenType) => {
  wsCache.set(RefreshTokenKey, token.refreshToken)
  wsCache.set(AccessTokenKey, token.accessToken)
}

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

/** 格式化token(jwt格式) */
export const formatToken = (token: string): string => {
  return 'Bearer ' + token
}

后端Java的token只需校验即可:

相关推荐
安之若素^4 分钟前
启用不安全的HTTP方法
java·开发语言
ruanjiananquan9910 分钟前
c,c++语言的栈内存、堆内存及任意读写内存
java·c语言·c++
chuanauc37 分钟前
Kubernets K8s 学习
java·学习·kubernetes
一头生产的驴1 小时前
java整合itext pdf实现自定义PDF文件格式导出
java·spring boot·pdf·itextpdf
YuTaoShao1 小时前
【LeetCode 热题 100】73. 矩阵置零——(解法二)空间复杂度 O(1)
java·算法·leetcode·矩阵
zzywxc7871 小时前
AI 正在深度重构软件开发的底层逻辑和全生命周期,从技术演进、流程重构和未来趋势三个维度进行系统性分析
java·大数据·开发语言·人工智能·spring
YuTaoShao3 小时前
【LeetCode 热题 100】56. 合并区间——排序+遍历
java·算法·leetcode·职场和发展
程序员张33 小时前
SpringBoot计时一次请求耗时
java·spring boot·后端
llwszx6 小时前
深入理解Java锁原理(一):偏向锁的设计原理与性能优化
java·spring··偏向锁
云泽野7 小时前
【Java|集合类】list遍历的6种方式
java·python·list