目录
- 前言
- 1. 基本知识
- 2. Demo
- 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实现
登录组件
<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>javascript">
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>
请求拦截器
// 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实现
登录页面
<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>javascript">
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 请求拦截器
// 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 {@Autowiredprivate 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 {@Autowiredprivate JwtUtil jwtUtil;@Autowiredprivate 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 {@Overrideprotected 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 {@Autowiredprivate JwtUtil jwtUtil;@Overrideprotected 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请求:
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 => {// 是否需要设置 tokenconst isToken = (config.headers || {}).isToken === falseconfig.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] = responseif (error) {toast('后端接口连接异常')reject('后端接口连接异常')return}const code = res.data.code || 200const 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 } = errorif (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都是存放本地:
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的格式:
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只需校验即可: