實(shí)戰(zhàn):十分鐘實(shí)現(xiàn)基于JWT前后端分離的權(quán)限框架

## 前言

面試過(guò)很多Java開(kāi)發(fā),能把權(quán)限這塊說(shuō)的清楚的實(shí)在是不多,很多人因?yàn)楣卷?xiàng)目職責(zé)問(wèn)題,很難學(xué)到這類相關(guān)的流程和技術(shù),本文梳理一個(gè)簡(jiǎn)單的場(chǎng)景,實(shí)現(xiàn)一個(gè)基于jwt前后端分離的權(quán)限框架。

## 簡(jiǎn)易流程

### 登錄獲取票據(jù)和緩存信息

![image-20200709160301317](https://tva1.sinaimg.cn/large/007S8ZIlgy1ggks58pat8j310a0fgwfr.jpg)

### 鑒權(quán)流程

![image-20200709160427929](https://tva1.sinaimg.cn/large/007S8ZIlgy1ggks6pkk94j311o0eegn7.jpg)

## 技術(shù)棧和功能規(guī)劃

本文技術(shù)選型為SpringBoot+JWT+Redis, 實(shí)現(xiàn)上圖的登錄流程和鑒權(quán)流程,并提供完整項(xiàng)目代碼。

**本項(xiàng)目已實(shí)現(xiàn)如下功能**:

- 跨域配置

- jwt集成

- redis集成

- BaseController封裝,方便取出用戶信息

- 攔截器和白名單

- 全局異常

- jwt工具類封裝

- redis工具類封裝

- redis枚舉Key封裝

### Redis安裝和啟動(dòng)

使用Docker一行命令構(gòu)建啟動(dòng)Redis,命令如下

```

docker run -itd --name redis-test -p 6379:6379 redis --requirepass "123456redis"

```

指定端口:`6379`

指定密碼:`123456redis`

![](https://tva1.sinaimg.cn/large/007S8ZIlgy1ggksnzgu5hj31ka03yjs0.jpg)

客戶端連接測(cè)試,問(wèn)題不大~

![](https://tva1.sinaimg.cn/large/007S8ZIlgy1ggksu4459hj30xa0egmzn.jpg)

### 創(chuàng)建SpringBoot項(xiàng)目并引入JWT依賴

```

<dependency>

? <groupId>io.jsonwebtoken</groupId>

? <artifactId>jjwt</artifactId>

? <version>0.9.1</version>

</dependency>

```

其實(shí)主要是引了這個(gè)包,其他的就不貼出來(lái)了。主要有redis以及json相關(guān)的,完整代碼都在項(xiàng)目可以自己看

## 寫(xiě)一個(gè)登陸方法

`UserController`

```java

? @PostMapping("/login")

? ? public UserVO login(@RequestBody LoginDTO loginDTO)? {

? ? ? ? return userService.login(loginDTO);

? ? }

```

`UserService`

用戶信息就模擬的了,都看的懂。登錄成功后根據(jù)uid生產(chǎn)jwt,然后緩存到redis,封裝結(jié)果給到前端

```java

package com.lzp.auth.service;

@Service

public class UserService {

? ? @Value("${server.session.timeout:3000}")

? ? private Long timeout;

? ? @Autowired

? ? private RedisUtils redisUtils;

? ? final static String USER_NAME = "admin";

? ? //密碼 演示用就不做加密處理了

? ? final static String PWD = "admin";

? ? public UserVO login(LoginDTO loginDTO){

? ? ? ? User user = getByName(loginDTO.getUserName());

? ? ? ? //用戶信息校驗(yàn)和查詢

? ? ? ? if (user == null){

? ? ? ? ? ? throw new ServiceException(ResultCodeEnum.LOGIN_FAIL);

? ? ? ? }

? ? ? ? //密碼校驗(yàn)

? ? ? ? if(!PWD.equals(loginDTO.getPwd())){

? ? ? ? ? ? throw new ServiceException(ResultCodeEnum.LOGIN_FAIL);

? ? ? ? }

? ? ? ? //緩存用戶信息并設(shè)置過(guò)期時(shí)間

? ? ? ? UserVO userVO = new UserVO();

? ? ? ? userVO.setName(user.getName());

? ? ? ? userVO.setUid(user.getUid());

? ? ? ? userVO.setToken(JWTUtils.generate(user.getUid()));

? ? ? ? //信息入庫(kù)redis

? ? ? ? redisUtils.set(RedisKeyEnum.OAUTH_APP_TOKEN.keyBuilder(userVO.getUid()), JSONObject.toJSONString(userVO), timeout);

? ? ? ? return userVO;

? ? }

? ? /**

? ? * 通過(guò)用戶名獲取用戶

? ? * @param name

? ? * @return

? ? */

? ? public User getByName(String name){

? ? ? ? User user = null;

? ? ? ? if(USER_NAME.equals(name)){

? ? ? ? ? ? user =? new User("1","張三","Aa123456");

? ? ? ? }

? ? ? ? return user;

? ? }

}

```

### 定義登錄攔截器

`LoginInterceptor`

```java

package com.lzp.auth.config;

import com.lzp.auth.exception.ServiceException;

import com.lzp.auth.utils.JWTUtils;

import com.lzp.auth.utils.ResultCodeEnum;

import com.lzp.auth.utils.SessionContext;

import io.jsonwebtoken.Claims;

import lombok.extern.slf4j.Slf4j;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.data.redis.core.StringRedisTemplate;

import org.springframework.util.StringUtils;

import org.springframework.web.servlet.ModelAndView;

import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;

@Slf4j

public class LoginInterceptor extends HandlerInterceptorAdapter {

? ? @Autowired

? ? StringRedisTemplate redisTemplate;

? ? @Override

? ? public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

? ? ? ? String token = request.getHeader("token");

? ? ? ? String requestURI = request.getRequestURI().replaceAll("/+", "/");

? ? ? ? log.info("requestURI:{}",requestURI);

? ? ? ? if (StringUtils.isEmpty(token)) {

? ? ? ? ? ? throw new ServiceException(ResultCodeEnum.AUTH_FAIL);

? ? ? ? }

? ? ? ? Claims claim = JWTUtils.getClaim(token);

? ? ? ? if(claim == null){

? ? ? ? ? ? throw new ServiceException(ResultCodeEnum.AUTH_FAIL);

? ? ? ? }

? ? ? ? String uid = null;

? ? ? ? try {

? ? ? ? ? ? uid = JWTUtils.getOpenId(token);

? ? ? ? } catch (Exception e) {

? ? ? ? ? ? throw new ServiceException(ResultCodeEnum.AUTH_FAIL);

? ? ? ? }

? ? ? ? //用戶id放到上下文 可以當(dāng)前請(qǐng)求進(jìn)行傳遞

? ? ? ? request.setAttribute(SessionContext.USER_ID_KEY, uid);

? ? ? ? return true;

? ? }

? ? @Override

? ? public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

? ? }

? ? @Override

? ? public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

? ? }

}

```

### 配置跨域、資源、定義攔截器、加白名單

```java

package com.lzp.auth.config;

import org.springframework.beans.BeansException;

import org.springframework.context.ApplicationContext;

import org.springframework.context.ApplicationContextAware;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.web.cors.CorsConfiguration;

import org.springframework.web.cors.reactive.CorsWebFilter;

import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;

import org.springframework.web.servlet.config.annotation.InterceptorRegistry;

import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;

import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

import org.springframework.web.util.pattern.PathPatternParser;

@Configuration

public class WebMvcConfig extends WebMvcConfigurerAdapter implements ApplicationContextAware {

? ? private ApplicationContext applicationContext;

? ? public WebMvcConfig() {

? ? ? ? super();

? ? }

? ? @Override

? ? public void addResourceHandlers(ResourceHandlerRegistry registry) {

? ? ? ? super.addResourceHandlers(registry);

? ? }

? ? @Override

? ? public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {

? ? ? ? this.applicationContext = applicationContext;

? ? }

? ? @Bean

? ? public CorsWebFilter corsFilter() {

? ? ? ? CorsConfiguration config = new CorsConfiguration();

? ? ? ? config.addAllowedMethod("*");

? ? ? ? config.addAllowedOrigin("*");

? ? ? ? config.addAllowedHeader("*");

? ? ? ? UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());

? ? ? ? source.registerCorsConfiguration("/**", config);

? ? ? ? return new CorsWebFilter(source);

? ? }

? ? @Bean

? ? LoginInterceptor loginInterceptor() {

? ? ? ? return new LoginInterceptor();

? ? }

? ? @Override

? ? public void addInterceptors(InterceptorRegistry registry) {

? ? ? ? //攔截規(guī)則:除了login,其他都攔截判斷

? ? ? ? registry.addInterceptor(loginInterceptor())

? ? ? ? ? ? ? ? .addPathPatterns("/**")

? ? ? ? ? ? ? ? .excludePathPatterns("/user/login");

? ? ? ? super.addInterceptors(registry);

? ? }

}

```

**postMan調(diào)試**

模擬成功和失敗場(chǎng)景

![](https://tva1.sinaimg.cn/large/007S8ZIlgy1ggpccb0ziaj311u0u0djv.jpg)

成功如上圖,返回用戶信息和Token

![](https://tva1.sinaimg.cn/large/007S8ZIlgy1ggpcctq973j30z50u0got.jpg)

失敗如上圖

### 測(cè)試非白名單接口

![](https://tva1.sinaimg.cn/large/007S8ZIlgy1ggpcihzfg9j30ze0tawi9.jpg)

![](https://tva1.sinaimg.cn/large/007S8ZIlgy1ggpci6s075j30y20n6q54.jpg)

**使用token再來(lái)訪問(wèn)當(dāng)前接口**

![](https://tva1.sinaimg.cn/large/007S8ZIlgy1ggpcs9x8t9j31i00oytbj.jpg)

完美~

至此本項(xiàng)目就完美結(jié)束了哦

### 項(xiàng)目代碼

https://github.com/pengziliu/GitHub-code-practice

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。