简介

JWT 全称 JSON Web Token
作用是用户授权( Authorization ), 而不是用户的身份认证( Authentication )

用户认证: 通过用户名和密码验证用户身份
用户授权: 使用户有权限访问特定资源

JWT 由 3 部分组成

  • HEADER
  • PAYLOAD
  • VARIFY SIGNATURE

https://jwt.io/
image.png

传统的Session登录 (有状态登录)

用户登录后,服务器创建并保存 session, 并将 sessionID 发送并保存到客户端的 cookie 中,客户端每次发http请求 cookie 都携带 sessionID, 服务器通过 sessionID 验证用户的身份和权限

JWT (无状态登录)

用户登录后服务器通过私钥加密(非对称加密算法RSA)生成 Token 返回给客户端,客户端发送请求时携带 Token, 服务器通过私钥解密Token 验证, 因为 JWT 只保存在客户端,属于无状态登录

JWT 优点

  • 无状态,简单、方便、完美支持分布式部署
  • 非对称加密,Token安全性高

JWT 缺点

  • 无状态,token一经发布则无法取消
  • 明文传递,Token安全性低 (https可解决)

实现登录和注销逻辑

创建userSlice

  1. import axios from 'axios'
  2. import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit'
  3. interface UserState {
  4. loading: boolean
  5. error: string | null
  6. token: string | null
  7. }
  8. const initialState: UserState = {
  9. loading: false,
  10. error: null,
  11. token: null,
  12. }
  13. export const signIn = createAsyncThunk(
  14. 'user/signIn', // type
  15. async (
  16. parameter: {
  17. username: string
  18. password: string
  19. },
  20. thunkAPI
  21. ) => {
  22. const { data } = await axios.post('/auth/login', {
  23. username: parameter.username,
  24. password: parameter.password,
  25. })
  26. return data.token
  27. }
  28. )
  29. export const userSlice = createSlice({
  30. name: 'user',
  31. initialState,
  32. reducers: {},
  33. extraReducers: {
  34. [signIn.pending.type]: (state) => {
  35. state.loading = true
  36. },
  37. [signIn.fulfilled.type]: (state, action) => {
  38. state.loading = false
  39. state.token = action.payload
  40. state.error = null
  41. },
  42. [signIn.rejected.type]: (state, action: PayloadAction<string | null>) => {
  43. state.error = action.payload
  44. state.loading = false
  45. },
  46. },
  47. })

将 userSlice 放到 rootReducer 中

  1. const rootReducer = combineReducers({
  2. language: languageReducer,
  3. recommendProducts: recommendProductsReducer,
  4. productDetail: productDetailSlice.reducer,
  5. user: userSlice.reducer,
  6. })

点击登录获取到 jwt 后跳转到首页

  1. export const SignInForm = () => {
  2. const nevigate = useNavigate()
  3. const loading = useSelector((state) => state.user.loading)
  4. const jwt = useSelector((state) => state.user.token)
  5. const error = useSelector((state) => state.user.error)
  6. const dispatch = useDispatch()
  7. useEffect(() => {
  8. if (jwt !== null) nevigate('/')
  9. }, [jwt]) // 当 jwt 变化时跳转首页
  10. const onFinish = (values: any) => {
  11. dispatch(
  12. signIn({
  13. username: values.username,
  14. password: values.password,
  15. })
  16. )
  17. }
  18. // ...
  19. return (
  20. <Form
  21. {...layout}
  22. name="basic"
  23. initialValues={{ remember: true }}
  24. onFinish={onFinish}
  25. onFinishFailed={onFinishFailed}
  26. className={styles['register-form']}
  27. >
  28. // ...
  29. <Form.Item {...tailLayout}>
  30. <Button type="primary" htmlType="submit" loading={loading}> {// loading为true时显示加载中}
  31. Submit
  32. </Button>
  33. </Form.Item>
  34. </Form>
  35. )
  36. }

解码 jwt

安装 jwt-decode

  1. yarn add jwt-decode

解码 payload 字段

  1. import jwt_decode, { JwtPayload as DefaultJwtPayload } from 'jwt-decode'
  2. interface JwtPayload extends DefaultJwtPayload {
  3. username: string
  4. }
  5. export const Header: React.FC = () => {
  6. // ...
  7. const [username, setUsername] = useState('')
  8. const jwt = useSelector((state) => state.user.token)
  9. useEffect(() => {
  10. if (jwt !== null) {
  11. setUsername(jwt_decode<JwtPayload>(jwt).username)
  12. }
  13. }, [jwt])
  14. // ...
  15. }

注销

在 userSlice 中添加 reducers

  1. export const userSlice = createSlice({
  2. name: 'user',
  3. initialState,
  4. reducers: {
  5. signOut: (state) => {
  6. state.error = null
  7. state.loading = false
  8. state.token = null
  9. },
  10. },
  11. extraReducers: {
  12. // ...
  13. },
  14. })

点击注销事件处理

  1. const onSignOut = () => {
  2. dispatch(userSlice.actions.signOut())
  3. navigate('/')
  4. window.location.reload() // 刷新页面,可选
  5. }

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

安装

  1. yarn add redux-persist

修改 store.ts

  1. // ...
  2. import { persistStore, persistReducer } from 'redux-persist'
  3. import storage from 'redux-persist/lib/storage' // 默认是localStorage
  4. const persistConfig = {
  5. key: 'root',
  6. storage,
  7. whitelist: ['user'], // 即rootReducer中的user, 在白名单中的才会存储到localStorage
  8. }
  9. const rootReducer = combineReducers({
  10. language: languageReducer,
  11. recommendProducts: recommendProductsReducer,
  12. productDetail: productDetailSlice.reducer,
  13. user: userSlice.reducer,
  14. })
  15. const persistedReducer = persistReducer(persistConfig, rootReducer)
  16. const store = configureStore({
  17. reducer: persistedReducer, // 使用 persistedReducer 替代 rootReducer
  18. middleware: (getDefaultMiddleware) => [...getDefaultMiddleware(), actionLog],
  19. devTools: true,
  20. })
  21. const persistor = persistStore(store)
  22. export type RootState = ReturnType<typeof store.getState>
  23. export type AppDispatch = typeof store.dispatch
  24. export default { store, persistor }

修改入口文件 index.ts

  1. // ...
  2. import rootStore from './redux/store'
  3. import { PersistGate } from 'redux-persist/integration/react'
  4. ReactDOM.render(
  5. <React.StrictMode>
  6. <Provider store={rootStore.store}>
  7. <PersistGate loading={null} persistor={rootStore.persistor}>
  8. <App />
  9. </PersistGate>
  10. </Provider>
  11. </React.StrictMode>,
  12. document.getElementById('root')
  13. )

PersistGate 的 loading 属性可以设置过渡动画, 例如 antd 的 Spin 组件