目录
- 前言
- [1. 基本知识](#1. 基本知识)
- [2. Demo](#2. Demo)
- [3. 实战](#3. 实战)
前言
🤟 找工作,来万码优才:👉 #小程序://万码优才/r6rqmzDaXpYkJZF
1. 基本知识
Bearer Token是一种基于Token的认证机制,用于在HTTP请求中传递用户的身份信息
应用于RESTful API和各种Web应用中,提供了一种轻量且高效的身份验证方式
基本的作用如下:
- 身份验证:通过Token验证用户的身份,确定其是否有权访问某个资源
- 授权:Token中可以包含用户的权限信息,服务器可以根据Token中的信息决定用户可以进行的操作
- 无状态认证:服务器不需要保存用户的会话状态,只需解析Token即可验证用户身份,使系统更易于扩展和管理
具体的工作原理如下:
- 用户登录:用户向服务器发送登录请求,提供用户名和密码等认证信息
- Token生成:服务器验证用户信息后,生成一个包含用户身份和权限信息的Bearer Token。 Token可以是纯字符串,也可以是经过签名的JWT(JSON Web Token)
- Token传输:服务器将生成的Token返回给客户端,客户端将其存储在本地(如浏览器的Local Storage或Cookie中)
- 请求资源:客户端在后续的HTTP请求中,将Bearer Token放在Authorization头中,发送到服务器
- 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的步骤
- 用户登录:客户端发送登录请求,携带用户名和密码;服务器验证用户信息,成功后生成Bearer Token;返回Token给客户端
- 存储Token:客户端将Token存储在安全的位置,如HTTP-only Cookie或Local Storage中;确保Token不会被恶意脚本窃取,推荐使用HTTPS传输
- 发送请求:客户端在后续请求中,在Authorization头添加"Bearer ";服务器接收到请求后,解析Token,验证用户身份
- Token验证:服务器检查Token的签名是否有效,确保Token未被篡改;验证Token是否过期,获取用户信息和权限;授权或拒绝请求
Bearer Token的过期与刷新
过期时间定义Token的存活时间,通常几分钟到几小时不等。Token过期后,用户需要重新登录以获取新的Token
刷新Token:用户在Token过期前,可以请求刷新Token,获取新的有效Token
- 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');
- 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只需校验即可:
