简介
JWT 全称 JSON Web Token
作用是用户授权( Authorization ), 而不是用户的身份认证( Authentication )
用户认证: 通过用户名和密码验证用户身份
用户授权: 使用户有权限访问特定资源
JWT 由 3 部分组成
- HEADER
- PAYLOAD
- VARIFY SIGNATURE
传统的Session登录 (有状态登录)
用户登录后,服务器创建并保存 session, 并将 sessionID 发送并保存到客户端的 cookie 中,客户端每次发http请求 cookie 都携带 sessionID, 服务器通过 sessionID 验证用户的身份和权限
JWT (无状态登录)
用户登录后服务器通过私钥加密(非对称加密算法RSA)生成 Token 返回给客户端,客户端发送请求时携带 Token, 服务器通过私钥解密Token 验证, 因为 JWT 只保存在客户端,属于无状态登录
JWT 优点
- 无状态,简单、方便、完美支持分布式部署
- 非对称加密,Token安全性高
JWT 缺点
- 无状态,token一经发布则无法取消
- 明文传递,Token安全性低 (https可解决)
实现登录和注销逻辑
创建userSlice
import axios from 'axios'import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit'interface UserState {loading: booleanerror: string | nulltoken: string | null}const initialState: UserState = {loading: false,error: null,token: null,}export const signIn = createAsyncThunk('user/signIn', // typeasync (parameter: {username: stringpassword: string},thunkAPI) => {const { data } = await axios.post('/auth/login', {username: parameter.username,password: parameter.password,})return data.token})export const userSlice = createSlice({name: 'user',initialState,reducers: {},extraReducers: {[signIn.pending.type]: (state) => {state.loading = true},[signIn.fulfilled.type]: (state, action) => {state.loading = falsestate.token = action.payloadstate.error = null},[signIn.rejected.type]: (state, action: PayloadAction<string | null>) => {state.error = action.payloadstate.loading = false},},})
将 userSlice 放到 rootReducer 中
const rootReducer = combineReducers({language: languageReducer,recommendProducts: recommendProductsReducer,productDetail: productDetailSlice.reducer,user: userSlice.reducer,})
点击登录获取到 jwt 后跳转到首页
export const SignInForm = () => {const nevigate = useNavigate()const loading = useSelector((state) => state.user.loading)const jwt = useSelector((state) => state.user.token)const error = useSelector((state) => state.user.error)const dispatch = useDispatch()useEffect(() => {if (jwt !== null) nevigate('/')}, [jwt]) // 当 jwt 变化时跳转首页const onFinish = (values: any) => {dispatch(signIn({username: values.username,password: values.password,}))}// ...return (<Form{...layout}name="basic"initialValues={{ remember: true }}onFinish={onFinish}onFinishFailed={onFinishFailed}className={styles['register-form']}>// ...<Form.Item {...tailLayout}><Button type="primary" htmlType="submit" loading={loading}> {// loading为true时显示加载中}Submit</Button></Form.Item></Form>)}
解码 jwt
安装 jwt-decode
yarn add jwt-decode
解码 payload 字段
import jwt_decode, { JwtPayload as DefaultJwtPayload } from 'jwt-decode'interface JwtPayload extends DefaultJwtPayload {username: string}export const Header: React.FC = () => {// ...const [username, setUsername] = useState('')const jwt = useSelector((state) => state.user.token)useEffect(() => {if (jwt !== null) {setUsername(jwt_decode<JwtPayload>(jwt).username)}}, [jwt])// ...}
注销
在 userSlice 中添加 reducers
export const userSlice = createSlice({name: 'user',initialState,reducers: {signOut: (state) => {state.error = nullstate.loading = falsestate.token = null},},extraReducers: {// ...},})
点击注销事件处理
const onSignOut = () => {dispatch(userSlice.actions.signOut())navigate('/')window.location.reload() // 刷新页面,可选}
redux-persist 登录持久化
Cookie、session和web Storage
- cookie 和 webStorage 保存在浏览器中;session 保存于服务器上
- cookie 不超过4K, web storage 上限是5M, session 无上限
- cookie 和 webStorage 安全性差,session 性能差
- cookie 在 http 请求中会被自动携带; web Storage不会自动发送
Web Storage 好处
- 有效降低网络流量
- 快速显示数据
- 临时存储
Web Storage 类型
- sessionStorage: 仅当前浏览器窗口关闭之前有效
- localStorage: 始终有效
redux-persist
redux-persist 默认将 store 存储到 localStorage
安装
yarn add redux-persist
修改 store.ts
// ...import { persistStore, persistReducer } from 'redux-persist'import storage from 'redux-persist/lib/storage' // 默认是localStorageconst persistConfig = {key: 'root',storage,whitelist: ['user'], // 即rootReducer中的user, 在白名单中的才会存储到localStorage}const rootReducer = combineReducers({language: languageReducer,recommendProducts: recommendProductsReducer,productDetail: productDetailSlice.reducer,user: userSlice.reducer,})const persistedReducer = persistReducer(persistConfig, rootReducer)const store = configureStore({reducer: persistedReducer, // 使用 persistedReducer 替代 rootReducermiddleware: (getDefaultMiddleware) => [...getDefaultMiddleware(), actionLog],devTools: true,})const persistor = persistStore(store)export type RootState = ReturnType<typeof store.getState>export type AppDispatch = typeof store.dispatchexport default { store, persistor }
修改入口文件 index.ts
// ...import rootStore from './redux/store'import { PersistGate } from 'redux-persist/integration/react'ReactDOM.render(<React.StrictMode><Provider store={rootStore.store}><PersistGate loading={null} persistor={rootStore.persistor}><App /></PersistGate></Provider></React.StrictMode>,document.getElementById('root'))
PersistGate 的 loading 属性可以设置过渡动画, 例如 antd 的 Spin 组件

