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

前言

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

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

  1. 如何使用springboot褐墅,springSecurity拆檬,jwt實現(xiàn)基于token的權(quán)限管理
  2. 統(tǒng)一處理無權(quán)限請求的結(jié)果

JWT

再稍微提一提jwt吧,在前段時間有個小項目是前后端分離的妥凳,所以需要用到基于token的權(quán)限管理機制竟贯,所以就了解到了jwt這一個方案。不過關(guān)于這個方案逝钥,似乎沒有一個如何管理已經(jīng)生產(chǎn)的token的方法(如果有的話歡迎告知澄耍,我還不知道呢。晌缘。)一旦生成了一個token齐莲,就無法對該token進行任何操作,無法使該token失效磷箕,只有等到該token到了過期的時間點才失效选酗,這樣就會有一個很大的隱患。然后搜索了挺多相關(guān)的資料以及經(jīng)過相當長一段時間的思考決定使用redis去管理已經(jīng)生成的token岳枷,下面會詳細說一下芒填。

整理一下思路

創(chuàng)建一個新工程時,我們需要思考一下我們接下來需要的一些步驟空繁,需要做什么殿衰,怎么做。

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

創(chuàng)建一個springboot工程

建議使用maven去構(gòu)建項目闷祥。

        <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

創(chuàng)建一個演示的實體類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封裝一下方便調(diào)用

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;

    // 創(chuàng)建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

寫一個根據(jù)用戶名獲取用戶的方法,后續(xù)會用到

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

UserDetailsServiceImpl

使用springSecurity需要實現(xiàn)UserDetailsService接口供權(quán)限框架調(diào)用栓拜,該方法只需要實現(xiàn)一個方法就可以了座泳,那就是根據(jù)用戶名去獲取用戶,那就是上面repository定義的方法了幕与,這里直接調(diào)用了挑势。

@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類型的接口,所以這邊就再寫一個類去實現(xiàn)一下這個接口纽门。

JwtUser

實現(xiàn)這個接口需要實現(xiàn)幾個方法

public class JwtUser implements UserDetails {

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

    public JwtUser() {
    }

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

    // 獲取權(quán)限信息薛耻,目前博主只會拿來存角色营罢。赏陵。
    @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 +
                '}';
    }
}

配置攔截器

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

回到正題端铛,到底要怎么配置呢泣矛?使用過shiro的人會知道,鑒權(quán)的話需要自己實現(xiàn)一個realm禾蚕,重寫兩個方法您朽,第一是用戶驗證,第二是鑒權(quán)换淆。在spring-security中也不例外虚倒,這邊需要實現(xiàn)兩個過濾器。使用JWTAuthenticationFilter去進行用戶賬號的驗證产舞,使用JWTAuthorizationFilter去進行用戶權(quán)限的驗證魂奥。

JWTAuthenticationFilter

JWTAuthenticationFilter繼承于UsernamePasswordAuthenticationFilter
該攔截器用于獲取用戶登錄的信息,只需創(chuàng)建一個token并調(diào)用authenticationManager.authenticate()讓spring-security去進行驗證就可以了易猫,不用自己查數(shù)據(jù)庫再對比密碼了耻煤,這一步交給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;
        }
    }

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

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

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

JWTAuthorizationFilter

驗證成功當然就是進行鑒權(quán)了,每一次需要權(quán)限的請求都需要檢查該用戶是否有該權(quán)限去操作該資源样勃,當然這也是框架幫我們做的吠勘,那么我們需要做什么呢性芬?很簡單,只要告訴spring-security該用戶是否已登錄剧防,是什么角色植锉,擁有什么權(quán)限就可以了。
JWTAuthenticationFilter繼承于BasicAuthenticationFilter峭拘,至于為什么要繼承這個我也不太清楚了俊庇,這個我也是網(wǎng)上看到的其中一種實現(xiàn),實在springSecurity苦手鸡挠,不過我覺得不繼承這個也沒事呢(實現(xiàn)以下filter接口或者繼承其他filter實現(xiàn)子類也可以吧)只要確保過濾器的順序辉饱,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拣展,則進行解析鞋囊,并且設(shè)置認證信息
        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

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

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

    @Autowired
    // 因為UserDetailsService的實現(xiàn)類實在太多啦,這里設(shè)置一下我們要注入的實現(xiàn)類
    @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的構(gòu)造方法中加入下面那一句話就可以啦

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

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

TaskController

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

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

    @GetMapping
    public String listTasks(){
        return "任務列表";
    }
    
    @PostMapping
    public String newTasks(){
        return "創(chuàng)建了一個新的任務";
    }
    
    @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比較直觀明了了贰剥。下面先注冊一下賬號倾剿,這里返回了插入了數(shù)據(jù)庫之后的用戶實體,所以注冊是成功了

注冊

注冊成功

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


未登錄403

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

登錄

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

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

再次訪問一下tasks乖菱,結(jié)果理想當然的是成功啦~
成功請求

初期總結(jié)

到這里我們一個基礎(chǔ)的Springboot+SpringSecurity+Jwt已經(jīng)搭建好了。
到這里一個基本的jwt已經(jīng)實現(xiàn)了蓬网,但是總覺得哪里不對呢窒所,寫了這么多才只是登錄成功了?權(quán)限管理呢帆锋?token管理呢吵取?
確實,看一下上面的代碼锯厢。在實現(xiàn)UserDetails接口的時候?qū)懥艘恍┢婀值臇|西皮官,就是這個getAuthorities方法啦。
這是springSecurity用來獲取用戶權(quán)限的方法实辑。
在User類中寫得role在這里就能排上用場了捺氢,這里將要實現(xiàn)的權(quán)限管理是基于角色的權(quán)限管理,再細顆粒的博主就不會啦哈哈哈剪撬,但還是可以看一看的摄乒。

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

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

在springSecurity里建議角色名稱改成ROLE_統(tǒng)一前綴的角色,例如ROLE_USER,ROLE_ADMIN,ROLE_XXX残黑,至于為什么馍佑,后面會提到的,先不急梨水,這里先這樣干著拭荤。

基于角色的權(quán)限管理

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

  1. 用戶驗證成功,根據(jù)用戶名以及過期時間生成token
  2. 權(quán)限驗證踊沸,假如能從token中獲取用戶名就該token驗證成功
  3. 創(chuàng)建一個UsernamePasswordAuthenticationToken該token包含用戶的角色信息歇终,而不是一個空的ArrayList,查看一下源代碼是有以下一個構(gòu)造方法的逼龟。
    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中鑒權(quán)的邏輯

  1. 檢查請求頭中是否存在Authorization奕短,如果沒有直接放宜肉,如果有就對token進行解析
  2. 解析token,檢查是否能從token中取出username翎碑,如果有就算成功了
  3. 再根據(jù)該username創(chuàng)建一個UsernamePasswordAuthenticationToken對象就算成功了

可這發(fā)現(xiàn)根本就不關(guān)role什么事啊

沉思

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

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

可這不現(xiàn)實啊日杈,每一次請求都要查詢一下數(shù)據(jù)庫這種開銷這么大的操作當然是不行的遣铝。
思考一下,為什么是使用jwt而不是一個簡簡單單的UUID作為token呢莉擒。
jwt是由三部分組成的:

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

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

改造一下JwtTokenUtils

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

    // 修改一下創(chuàng)建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中存了權(quán)限信息,可以直接獲取翅帜,由于只有一個角色就這么干了
    Collection<? extends GrantedAuthority> authorities = jwtUser.getAuthorities();
    for (GrantedAuthority authority : authorities){
        role = authority.getAuthority();
    }
    // 根據(jù)用戶名姻檀,角色創(chuàng)建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;
    }

到這里基本上修改已經(jīng)完成了,接下來就可以測試一下了藕甩,再配置一下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

接下來可以測試了狭莱,繼續(xù)使用postman對tasks資源進行刪除僵娃,顯然不行。


測試刪除tasks

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


測試獲取tasks

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

刪除成功

到這里位置骤素,基于角色的權(quán)限管理基本操作都做了一遍了匙睹,現(xiàn)在來解答一下上面挖的一些坑

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

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

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

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

統(tǒng)一結(jié)果處理

當然會有一些需求是要統(tǒng)一處理被403響應的事件,很簡單,只要新建一個類JWTAuthenticationEntryPoint實現(xiàn)一下接口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 = "統(tǒng)一處理唁桩,原因:"+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());
    }

這是統(tǒng)一處理后的結(jié)果


這里寫圖片描述

享受成功的喜悅

到這里一個較為完善的權(quán)限管理已經(jīng)實現(xiàn)啦,如果哪里有不足或者出現(xiàn)錯誤可以告訴一下我荒澡,或者可以到GitHub上提個issue一起討論下报辱。

代碼地址

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

結(jié)語

至于為什么沒有redis碍现,沒有token管理,因為在我寫這篇文章的時候想了很多饥侵,感覺我現(xiàn)在的解決方案也不是特別好鸵赫,如果想知道的話可以到GitHub上找我衣屏,一起討論下躏升。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市狼忱,隨后出現(xiàn)的幾起案子膨疏,更是在濱河造成了極大的恐慌,老刑警劉巖钻弄,帶你破解...
    沈念sama閱讀 207,248評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件佃却,死亡現(xiàn)場離奇詭異,居然都是意外死亡窘俺,警方通過查閱死者的電腦和手機饲帅,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評論 2 381
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來瘤泪,“玉大人灶泵,你說我怎么就攤上這事《酝荆” “怎么了赦邻?”我有些...
    開封第一講書人閱讀 153,443評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長实檀。 經(jīng)常有香客問我惶洲,道長,這世上最難降的妖魔是什么膳犹? 我笑而不...
    開封第一講書人閱讀 55,475評論 1 279
  • 正文 為了忘掉前任恬吕,我火速辦了婚禮,結(jié)果婚禮上须床,老公的妹妹穿的比我還像新娘铐料。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 64,458評論 5 374
  • 文/花漫 我一把揭開白布余赢。 她就那樣靜靜地躺著芯义,像睡著了一般。 火紅的嫁衣襯著肌膚如雪妻柒。 梳的紋絲不亂的頭發(fā)上扛拨,一...
    開封第一講書人閱讀 49,185評論 1 284
  • 那天,我揣著相機與錄音举塔,去河邊找鬼绑警。 笑死,一個胖子當著我的面吹牛央渣,可吹牛的內(nèi)容都是我干的计盒。 我是一名探鬼主播,決...
    沈念sama閱讀 38,451評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼芽丹,長吁一口氣:“原來是場噩夢啊……” “哼北启!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起拔第,我...
    開封第一講書人閱讀 37,112評論 0 261
  • 序言:老撾萬榮一對情侶失蹤咕村,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后蚊俺,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體懈涛,經(jīng)...
    沈念sama閱讀 43,609評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,083評論 2 325
  • 正文 我和宋清朗相戀三年泳猬,在試婚紗的時候發(fā)現(xiàn)自己被綠了批钠。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,163評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡得封,死狀恐怖埋心,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情呛每,我是刑警寧澤踩窖,帶...
    沈念sama閱讀 33,803評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站晨横,受9級特大地震影響洋腮,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜手形,卻給世界環(huán)境...
    茶點故事閱讀 39,357評論 3 307
  • 文/蒙蒙 一啥供、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧库糠,春花似錦伙狐、人聲如沸涮毫。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽罢防。三九已至,卻和暖如春唉侄,著一層夾襖步出監(jiān)牢的瞬間咒吐,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評論 1 261
  • 我被黑心中介騙來泰國打工属划, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留恬叹,地道東北人。 一個月前我還...
    沈念sama閱讀 45,636評論 2 355
  • 正文 我出身青樓同眯,卻偏偏與公主長得像绽昼,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子须蜗,可洞房花燭夜當晚...
    茶點故事閱讀 42,925評論 2 344

推薦閱讀更多精彩內(nèi)容