目錄
- 前言
- 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>
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>
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實體類
@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枚舉
public enum Role {USER, ADMIN
}
JwtConfig配置
@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工具類
@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
@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
@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
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只需校驗即可: