Springboot+Spring-Security+JWT+Redis實現restful Api的權限管理以及token管理(超詳細用愛發電版)

前言

其實挺早就想寫一篇關于jwt的博文去好好總結一下之前踩過的坑了,但是事情有點太多了,一直沒抽出時間來寫,剛好現在有點時間可以好好靜下來寫一遍(可能)有點質量的博文吧,畢竟一直都是看別人的博文去學習,我也好好寫一遍吧哈哈。既然如果偶然搜到這篇文章的話,我相信大家應該都了解了什么是jwt,比較想知道怎么使用springboot+spring-security去實現,當然也可以使用shiro,其實道理都差不多,可能看到標題可能會有疑問,為什么會有一個redis呢?這是我學習有關jwt相關知識的時候產生的一些問題,以及自己對這方面問題的一些解決方案,接下來的文章我會詳細跟大家討論一下的,歡迎大家也可以一起討論一下。(剛開始寫博客,寫的不好多多包涵)
由于簡書不能顯示目錄,不是特別方便,可以移步到我的個人博客進行查看。

看完這篇文章之后你可以知道

  1. 如何使用springboot,springSecurity,jwt實現基于token的權限管理
  2. 統一處理無權限請求的結果

JWT

再稍微提一提jwt吧,在前段時間有個小項目是前后端分離的,所以需要用到基于token的權限管理機制,所以就了解到了jwt這一個方案。不過關于這個方案,似乎沒有一個如何管理已經生產的token的方法(如果有的話歡迎告知,我還不知道呢。。)一旦生成了一個token,就無法對該token進行任何操作,無法使該token失效,只有等到該token到了過期的時間點才失效,這樣就會有一個很大的隱患。然后搜索了挺多相關的資料以及經過相當長一段時間的思考決定使用redis去管理已經生成的token,下面會詳細說一下。

整理一下思路

創建一個新工程時,我們需要思考一下我們接下來需要的一些步驟,需要做什么,怎么做。

  • 搭建springboot工程
  • 導入springSecurity跟jwt的依賴
  • 用戶的實體類
  • dao層
  • service層(真正開發時再寫,這里就直接調用dao層操作數據庫)
  • 實現UserDetailsService接口
  • 實現UserDetails接口
  • 驗證用戶登錄信息的攔截器
  • 驗證用戶權限的攔截器
  • springSecurity配置
  • 認證的Controller以及測試的controller
  • 測試
  • 享受成功的喜悅

創建一個springboot工程

建議使用maven去構建項目。

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>

實體類User

創建一個演示的實體類User,包含最基本的用戶名跟密碼,至于role干嘛用后面會提到

@Entity
@Table(name = "jd_user")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Integer id;

    @Column(name = "username")
    private String username;

    @Column(name = "password")
    private String password;

    @Column(name = "role")
    private String role;

    // getter and setter...
}

JWT工具類

這里jwt我選擇的是jjwt,至于為什么,可能是因為我用的比較順手吧(:3」∠)

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.0</version>
</dependency>

JwtTokenUtils

jwt工具類,對jjwt封裝一下方便調用

public class JwtTokenUtils {

    public static final String TOKEN_HEADER = "Authorization";
    public static final String TOKEN_PREFIX = "Bearer ";

    private static final String SECRET = "jwtsecretdemo";
    private static final String ISS = "echisan";

    // 過期時間是3600秒,既是1個小時
    private static final long EXPIRATION = 3600L;

    // 選擇了記住我之后的過期時間為7天
    private static final long EXPIRATION_REMEMBER = 604800L;

    // 創建token
    public static String createToken(String username, boolean isRememberMe) {
        long expiration = isRememberMe ? EXPIRATION_REMEMBER : EXPIRATION;
        return Jwts.builder()
                .signWith(SignatureAlgorithm.HS512, SECRET)
                .setIssuer(ISS)
                .setSubject(username)
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + expiration * 1000))
                .compact();
    }

    // 從token中獲取用戶名
    public static String getUsername(String token){
        return getTokenBody(token).getSubject();
    }

    // 是否已過期
    public static boolean isExpiration(String token){
        return getTokenBody(token).getExpiration().before(new Date());
    }

    private static Claims getTokenBody(String token){
        return Jwts.parser()
                .setSigningKey(SECRET)
                .parseClaimsJws(token)
                .getBody();
    }
}

UserRepository

寫一個根據用戶名獲取用戶的方法,后續會用到

public interface UserRepository extends CrudRepository<User, Integer> {
    User findByUsername(String username);
}

UserDetailsServiceImpl

使用springSecurity需要實現UserDetailsService接口供權限框架調用,該方法只需要實現一個方法就可以了,那就是根據用戶名去獲取用戶,那就是上面repository定義的方法了,這里直接調用了。

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(s);
        return new JwtUser(user);
    }

}

由于接口方法需要返回一個UserDetails類型的接口,所以這邊就再寫一個類去實現一下這個接口。

JwtUser

實現這個接口需要實現幾個方法

public class JwtUser implements UserDetails {

    private Integer id;
    private String username;
    private String password;
    private Collection<? extends GrantedAuthority> authorities;

    public JwtUser() {
    }

    // 寫一個能直接使用user創建jwtUser的構造器
    public JwtUser(User user) {
        id = user.getId();
        username = user.getUsername();
        password = user.getPassword();
        authorities = Collections.singleton(new SimpleGrantedAuthority(user.getRole()));
    }

    // 獲取權限信息,目前博主只會拿來存角色。。
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return username;
    }

    // 賬號是否未過期,默認是false,記得要改一下
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    // 賬號是否未鎖定,默認是false,記得也要改一下
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    // 賬號憑證是否未過期,默認是false,記得還要改一下
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    // 這個有點抽象不會翻譯,默認也是false,記得改一下
    @Override
    public boolean isEnabled() {
        return true;
    }

    // 我自己重寫打印下信息看的
    @Override
    public String toString() {
        return "JwtUser{" +
                "id=" + id +
                ", username='" + username + '\'' +
                ", password='" + password + '\'' +
                ", authorities=" + authorities +
                '}';
    }
}

配置攔截器

可以說到目前為止這是最復雜的一個步驟,其實搞清楚了還是挺簡單的,網上挺多人都更傾向于使用shiro,但是偶爾也要嘗試一下新東西的嘛,但是當時我在摸索的時候遇到挺多坑,當時也已經到了思考人生的地步了 框架不是為了簡化開發嗎!為什么!明明jwt加上權限框架是雙倍的快樂!為什么會這樣!(╯°口°)╯(┴—┴

回到正題,到底要怎么配置呢?使用過shiro的人會知道,鑒權的話需要自己實現一個realm,重寫兩個方法,第一是用戶驗證,第二是鑒權。在spring-security中也不例外,這邊需要實現兩個過濾器。使用JWTAuthenticationFilter去進行用戶賬號的驗證,使用JWTAuthorizationFilter去進行用戶權限的驗證。

JWTAuthenticationFilter

JWTAuthenticationFilter繼承于UsernamePasswordAuthenticationFilter
該攔截器用于獲取用戶登錄的信息,只需創建一個token并調用authenticationManager.authenticate()讓spring-security去進行驗證就可以了,不用自己查數據庫再對比密碼了,這一步交給spring去操作。
這個操作有點像是shiro的subject.login(new UsernamePasswordToken()),驗證的事情交給框架。
獻上這一部分的代碼。

public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    private AuthenticationManager authenticationManager;

    public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request,
                                                HttpServletResponse response) throws AuthenticationException {

        // 從輸入流中獲取到登錄的信息
        try {
            LoginUser loginUser = new ObjectMapper().readValue(request.getInputStream(), LoginUser.class);
            return authenticationManager.authenticate(
                    new UsernamePasswordAuthenticationToken(loginUser.getUsername(), loginUser.getPassword(), new ArrayList<>())
            );
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }

    // 成功驗證后調用的方法
    // 如果驗證成功,就生成token并返回
    @Override
    protected void successfulAuthentication(HttpServletRequest request,
                                            HttpServletResponse response,
                                            FilterChain chain,
                                            Authentication authResult) throws IOException, ServletException {

        // 查看源代碼會發現調用getPrincipal()方法會返回一個實現了`UserDetails`接口的對象
        // 所以就是JwtUser啦
        JwtUser jwtUser = (JwtUser) authResult.getPrincipal();
        System.out.println("jwtUser:" + jwtUser.toString());
        String token = JwtTokenUtils.createToken(jwtUser.getUsername(), false);
        // 返回創建成功的token
        // 但是這里創建的token只是單純的token
        // 按照jwt的規定,最后請求的格式應該是 `Bearer token`
        response.setHeader("token", JwtTokenUtils.TOKEN_PREFIX + token);
    }

    // 這是驗證失敗時候調用的方法
    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
        response.getWriter().write("authentication failed, reason: " + failed.getMessage());
    }
}

JWTAuthorizationFilter

驗證成功當然就是進行鑒權了,每一次需要權限的請求都需要檢查該用戶是否有該權限去操作該資源,當然這也是框架幫我們做的,那么我們需要做什么呢?很簡單,只要告訴spring-security該用戶是否已登錄,是什么角色,擁有什么權限就可以了。
JWTAuthenticationFilter繼承于BasicAuthenticationFilter,至于為什么要繼承這個我也不太清楚了,這個我也是網上看到的其中一種實現,實在springSecurity苦手,不過我覺得不繼承這個也沒事呢(實現以下filter接口或者繼承其他filter實現子類也可以吧)只要確保過濾器的順序,JWTAuthorizationFilterJWTAuthenticationFilter后面就沒問題了。

public class JWTAuthorizationFilter extends BasicAuthenticationFilter {

    public JWTAuthorizationFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain chain) throws IOException, ServletException {

        String tokenHeader = request.getHeader(JwtTokenUtils.TOKEN_HEADER);
        // 如果請求頭中沒有Authorization信息則直接放行了
        if (tokenHeader == null || tokenHeader.startsWith(JwtTokenUtils.TOKEN_PREFIX)) {
            chain.doFilter(request, response);
            return;
        }
        // 如果請求頭中有token,則進行解析,并且設置認證信息
        SecurityContextHolder.getContext().setAuthentication(getAuthentication(tokenHeader));
        super.doFilterInternal(request, response, chain);
    }

    // 這里從token中獲取用戶信息并新建一個token
    private UsernamePasswordAuthenticationToken getAuthentication(String tokenHeader) {
        String token = tokenHeader.replace(JwtTokenUtils.TOKEN_PREFIX, "");
        String username = JwtTokenUtils.getUsername(token);
        if (username != null){
            return new UsernamePasswordAuthenticationToken(username, null, new ArrayList<>());
        }
        return null;
    }
}

配置SpringSecurity

到這里基本操作都寫好啦,現在就需要我們將這些辛苦寫好的“組件”組合到一起發揮作用了,那就需要配置了。需要開啟一下注解@EnableWebSecurity然后再繼承一下WebSecurityConfigurerAdapter就可以啦,springboot就是可以為所欲為~

@EnableWebSecurity
// 至于為什么要配置這個,嘿嘿,賣個關子
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    // 因為UserDetailsService的實現類實在太多啦,這里設置一下我們要注入的實現類
    @Qualifier("userDetailsServiceImpl")
    private UserDetailsService userDetailsService;

    // 加密密碼的,安全第一嘛~
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors().and().csrf().disable()
                .authorizeRequests()
                // 測試用資源,需要驗證了的用戶才能訪問
                .antMatchers("/tasks/**").authenticated()
                // 其他都放行了
                .anyRequest().permitAll()
                .and()
                .addFilter(new JWTAuthenticationFilter(authenticationManager()))
                .addFilter(new JWTAuthorizationFilter(authenticationManager()))
                // 不需要session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }

    @Bean
    CorsConfigurationSource corsConfigurationSource() {
        final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues());
        return source;
    }
}

AuthController

連配置都搞定了,那么問題來了,沒有賬號密碼呢。所以寫一個注冊的控制器,這個就不是難事啦

@RestController
@RequestMapping("/auth")
public class AuthController {

    // 為了減少篇幅就不寫service接口了
    @Autowired
    private UserRepository userRepository;

    @Autowired
    private BCryptPasswordEncoder bCryptPasswordEncoder;

    @PostMapping("/register")
    public String registerUser(@RequestBody Map<String,String> registerUser){
        User user = new User();
        user.setUsername(registerUser.get("username"));
        // 記得注冊的時候把密碼加密一下
        user.setPassword(bCryptPasswordEncoder.encode(registerUser.get("password")));
        user.setRole("ROLE_USER");
        User save = userRepository.save(user);
        return save.toString();
    }
}

等等!注冊是有了,那登錄在哪呢?我們看一下UsernamePasswordAuthenticationFilter的源代碼

    public UsernamePasswordAuthenticationFilter() {
        super(new AntPathRequestMatcher("/login", "POST"));
    }

可以看出來默認是/login,所以登錄直接使用這個路徑就可以啦~當然也可以自定義
只需要在JWTAuthenticationFilter的構造方法中加入下面那一句話就可以啦

 public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
        super.setFilterProcessesUrl("/auth/login");
    }

所以現在認證的路徑統一了一下也是挺好的~看起來相當舒服了
注冊:/auth/register
登錄:/auth/login

TaskController

當然注冊登錄都完成了,那就是寫一個測試控制器,一個需要權限的控制器去測試了,為了控制一下文章篇幅,寫了一個比較簡單的控制器作為演示

@RestController
@RequestMapping("/tasks")
public class TaskController {

    @GetMapping
    public String listTasks(){
        return "任務列表";
    }
    
    @PostMapping
    public String newTasks(){
        return "創建了一個新的任務";
    }
    
    @PutMapping("/{taskId}")
    public String updateTasks(@PathVariable("taskId")Integer id){
        return "更新了一下id為:"+id+"的任務";
    }
    
    @DeleteMapping("/{taskId}")
    public String deleteTasks(@PathVariable("taskId")Integer id){
        return "刪除了id為:"+id+"的任務";
    }
}

測試

到這里基本操作都做好了,可以去測試一下了,這里使用的是postman比較直觀明了了。下面先注冊一下賬號,這里返回了插入了數據庫之后的用戶實體,所以注冊是成功了

注冊

注冊成功

接下來先測試一下先不登錄訪問一下我們的tasks,這里理所當然403無權限訪問了


未登錄403

然后終于能登錄了,接下來嘗試一下登錄之后再次訪問tasks看看是什么結果

登錄

發送了登錄請求之后查看響應頭,能看到我們生成后的token,那就是登錄成功了
登錄成功

接下來只需要把該響應頭添加到我們的請求頭上去,這里需要把Bearer[空格]去掉,注意Bearer后的空格也要去掉,因為postman再選了BearerToken之后會自動在token前面再加一個Bearer
設置請求頭

再次訪問一下tasks,結果理想當然的是成功啦~
成功請求

初期總結

到這里我們一個基礎的Springboot+SpringSecurity+Jwt已經搭建好了。
到這里一個基本的jwt已經實現了,但是總覺得哪里不對呢,寫了這么多才只是登錄成功了?權限管理呢?token管理呢?
確實,看一下上面的代碼。在實現UserDetails接口的時候寫了一些奇怪的東西,就是這個getAuthorities方法啦。
這是springSecurity用來獲取用戶權限的方法。
在User類中寫得role在這里就能排上用場了,這里將要實現的權限管理是基于角色的權限管理,再細顆粒的博主就不會啦哈哈哈,但還是可以看一看的。

    // 寫一個能直接使用user創建jwtUser的構造器
    public JwtUser(User user) {
        id = user.getId();
        username = user.getUsername();
        password = user.getPassword();
        // 這里只存儲了一個角色的名字
        authorities = Collections.singleton(new SimpleGrantedAuthority(user.getRole()));
    }

    // 獲取權限信息
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

在springSecurity里建議角色名稱改成ROLE_統一前綴的角色,例如ROLE_USER,ROLE_ADMIN,ROLE_XXX,至于為什么,后面會提到的,先不急,這里先這樣干著。

基于角色的權限管理

到底怎么基于角色的權限管理呢,這個只需要告訴權限框架該用戶擁有什么角色就可以了。但是吧要怎么告訴框架我什么角色呢。我們理一下如何實現基于角色的權限管理的思路

  1. 用戶驗證成功,根據用戶名以及過期時間生成token
  2. 權限驗證,假如能從token中獲取用戶名就該token驗證成功
  3. 創建一個UsernamePasswordAuthenticationToken該token包含用戶的角色信息,而不是一個空的ArrayList,查看一下源代碼是有以下一個構造方法的。
    public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
            Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        this.credentials = credentials;
        super.setAuthenticated(true); // must use super, as we override
    }

好了,接下來要怎么辦呢,可以往上滾動一下,再看一眼JWTAuthorizationFilter中鑒權的邏輯

  1. 檢查請求頭中是否存在Authorization,如果沒有直接放,如果有就對token進行解析
  2. 解析token,檢查是否能從token中取出username,如果有就算成功了
  3. 再根據該username創建一個UsernamePasswordAuthenticationToken對象就算成功了

可這發現根本就不關role什么事啊

沉思

    User user = userRepository.findByUsername("username");
    String role = user.getRole();
這里寫圖片描述

這還不簡單!這不就完事了嘛!

可這不現實啊,每一次請求都要查詢一下數據庫這種開銷這么大的操作當然是不行的。
思考一下,為什么是使用jwt而不是一個簡簡單單的UUID作為token呢。
jwt是由三部分組成的:

  1. 第一部分我們稱它為頭部(header)
  2. 第二部分我們稱其為載荷(payload)
  3. 第三部分是簽證(signature)

我們這里準備使用它的第二部分,使用payload去存儲我們的用戶角色信息,由于第一第二部分都是公開的,任何人都能知道里面的信息,不建議存儲一些比較敏感的數據,但是存放角色信息還是沒有問題的。

改造一下JwtTokenUtils

    // 添加角色的key
    private static final String ROLE_CLAIMS = "rol";

    // 修改一下創建token的方法
    public static String createToken(String username, String role, boolean isRememberMe) {
        long expiration = isRememberMe ? EXPIRATION_REMEMBER : EXPIRATION;
        HashMap<String, Object> map = new HashMap<>();
        map.put(ROLE_CLAIMS, role);
        return Jwts.builder()
                .signWith(SignatureAlgorithm.HS512, SECRET)
                // 這里要早set一點,放到后面會覆蓋別的字段
                .setClaims(map)
                .setIssuer(ISS)
                .setSubject(username)
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + expiration * 1000))
                .compact();
    }
    

修改JWTAuthenticationFilter

    JwtUser jwtUser = (JwtUser) authResult.getPrincipal();
    boolean isRemember = rememberMe.get() == 1;

    String role = "";
    // 因為在JwtUser中存了權限信息,可以直接獲取,由于只有一個角色就這么干了
    Collection<? extends GrantedAuthority> authorities = jwtUser.getAuthorities();
    for (GrantedAuthority authority : authorities){
        role = authority.getAuthority();
    }
    // 根據用戶名,角色創建token
    String token = JwtTokenUtils.createToken(jwtUser.getUsername(), role, isRemember);

修改JWTAuthorizationFilter

    // 這里從token中獲取用戶信息并新建一個token
    private UsernamePasswordAuthenticationToken getAuthentication(String tokenHeader) {
        String token = tokenHeader.replace(JwtTokenUtils.TOKEN_PREFIX, "");
        String username = JwtTokenUtils.getUsername(token);
        String role = JwtTokenUtils.getUserRole(token);
        if (username != null){
            return new UsernamePasswordAuthenticationToken(username, null, 
                    Collections.singleton(new SimpleGrantedAuthority(role))
            );
        }
        return null;
    }

到這里基本上修改已經完成了,接下來就可以測試一下了,再配置一下springSecurity

    protected void configure(HttpSecurity http) throws Exception {
        http.cors().and().csrf().disable()
                .authorizeRequests()
                // 測試用資源,需要驗證了的用戶才能訪問
                .antMatchers("/tasks/**").authenticated()
                // 需要角色為ADMIN才能刪除該資源
                .antMatchers(HttpMethod.DELETE, "/tasks/**").hasAuthority("ROLE_ADMIN")
                // 其他都放行了
                .anyRequest().permitAll()
                .and()
                .addFilter(new JWTAuthenticationFilter(authenticationManager()))
                .addFilter(new JWTAuthorizationFilter(authenticationManager()))
                // 不需要session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }

由于更新了token的生成方式,所以需要重新登錄一下獲取新的token

接下來可以測試了,繼續使用postman對tasks資源進行刪除,顯然不行。


測試刪除tasks

試試看獲取該資源會怎么樣,獲取tasks資源是沒有問題的。


測試獲取tasks

接下來重頭戲來了
先在數據庫里手動將admin的角色改成ROLE_ADMIN 修改完之后再登錄一下獲取新的token,再去嘗試一下刪除tasks資源
啪啪啪 成功啦~

刪除成功

到這里位置,基于角色的權限管理基本操作都做了一遍了,現在來解答一下上面挖的一些坑

  1. 為什么要以ROLE_作為前綴
  2. springSecurity中配置的注解@EnableGlobalMethodSecurity(prePostEnabled = true)是干嘛用的

第一個問題:
我們在springSecurity中配置了這樣一句,意思是只有角色為ROLE_ADMIN才有權限刪除該資源
.antMatchers(HttpMethod.DELETE, "/tasks/**").hasAuthority("ROLE_ADMIN")
假如我們使用了ROLE_作為前綴就能這樣寫了~是不是很方便呢哈哈
.antMatchers(HttpMethod.DELETE, "/tasks/**").hasRole("ADMIN")

第二個問題:
除了在springSecurity中配置訪問權限,還有這種方式啦,也是十分的方便呢。但是如果要使用這用的方式就需要配置上那個注解啦,不然雖然寫了下面的注解但是是不會生效的。

    @PostMapping
    @PreAuthorize("hasRole('ADMIN')")
    public String newTasks(){
        return "創建了一個新的任務";
    }

統一結果處理

當然會有一些需求是要統一處理被403響應的事件,很簡單,只要新建一個類JWTAuthenticationEntryPoint實現一下接口AuthenticationEntryPoint就可以了

public class JWTAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request,
                         HttpServletResponse response,
                         AuthenticationException authException) throws IOException, ServletException {

        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json; charset=utf-8");
        response.setStatus(HttpServletResponse.SC_FORBIDDEN);
        String reason = "統一處理,原因:"+authException.getMessage();
        response.getWriter().write(new ObjectMapper().writeValueAsString(reason));
    }
}

再配置一下springSecurity

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors().and().csrf().disable()
                .authorizeRequests()
                // 測試用資源,需要驗證了的用戶才能訪問
                .antMatchers("/tasks/**").authenticated()
                .antMatchers(HttpMethod.DELETE, "/tasks/**").hasRole("ADMIN")
                // 其他都放行了
                .anyRequest().permitAll()
                .and()
                .addFilter(new JWTAuthenticationFilter(authenticationManager()))
                .addFilter(new JWTAuthorizationFilter(authenticationManager()))
                // 不需要session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                // 加一句這個
                .exceptionHandling().authenticationEntryPoint(new JWTAuthenticationEntryPoint());
    }

這是統一處理后的結果


這里寫圖片描述

享受成功的喜悅

到這里一個較為完善的權限管理已經實現啦,如果哪里有不足或者出現錯誤可以告訴一下我,或者可以到GitHub上提個issue一起討論下。

代碼地址

Github: springboot-jwt-demo
代碼里也有挺多的注釋,可以看一看,如果覺得這篇文章幫助到你了可以到github點個小星星鼓勵一下博主~

結語

至于為什么沒有redis,沒有token管理,因為在我寫這篇文章的時候想了很多,感覺我現在的解決方案也不是特別好,如果想知道的話可以到GitHub上找我,一起討論下。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,835評論 6 534
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,676評論 3 419
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,730評論 0 380
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,118評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,873評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,266評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,330評論 3 443
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,482評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,036評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,846評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,025評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,575評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,279評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,684評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,953評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,751評論 3 394
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,016評論 2 375

推薦閱讀更多精彩內容