Spring Boot+JWT+Spring Security實現授權認證保護Rest API


 

 

 

 

 

通常情況下,把API直接暴露出去是風險很大的。那么一般來說,對API要划分出一定的權限級別,然后做一個用戶的鑒權,依據鑒權結果給予用戶開放對應的API。目前,比較主流的方案有幾種:

  1. 用戶名和密碼鑒權,使用Session保存用戶鑒權結果。
  2. 使用OAuth進行鑒權(其實OAuth也是一種基於Token的鑒權,只是沒有規定Token的生成方式)
  3. 自行采用Token進行鑒權

這里主要講一下JWT

JWT定義:

JWT是 Json Web Token 的縮寫。它是基於 RFC 7519 標准定義的一種可以安全傳輸的 小巧 和 自包含 的JSON對象。由於數據是使用數字簽名的,所以是可信任的和安全的。

JWT可以使用HMAC算法對secret進行加密或者使用RSA的公鑰私鑰對來進行簽名。

JWT的工作流程

下面是一個JWT的工作流程圖。模擬一下實際的流程是這樣的(假設受保護的API在/protected中)

  1. 用戶導航到登錄頁,輸入用戶名、密碼,進行登錄
  2. 服務器驗證登錄鑒權,如果改用戶合法,根據用戶的信息和服務器的規則生成JWT Token
  3. 服務器將該token以json形式返回(不一定要json形式,這里說的是一種常見的做法)
  4. 用戶得到token,存在localStorage、cookie或其它數據存儲形式中。
  5. 以后用戶請求/protected中的API時,在請求的header中加入 Authorization: Bearer xxxx(token)。此處注意token之前有一個7字符長度的 Bearer
  6. 服務器端對此token進行檢驗,如果合法就解析其中內容,根據其擁有的權限和自己的業務邏輯給出對應的響應結果。
  7. 用戶取得結果

 

添加maven依賴:

<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.10.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
<dependencies>
    <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter—web</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-thymeleaf</artifactId>
        </dependency>
<dependencies>

 JWT生成的代碼:

static public String createAccessToken(Authentication auth) {
        AccountCredentials credentials = (AccountCredentials) auth.getDetails();
        String userName = auth.getName();
        String role = credentials.isAdmin() ? "ROLE_ADMIN" : "ROLE_USER";
        Calendar nowTime = Calendar.getInstance();
        nowTime.add(Calendar.MINUTE, TOKENEXPIRATIONTIME);
        String accessToken = Jwts.builder().claim("authorities", role).claim("username", userName)
                .claim("userid", credentials.getUserId()).setSubject(userName).setIssuer(TOKENISSUER)
                .setIssuedAt(new Date(System.currentTimeMillis())).setExpiration(nowTime.getTime())
                .signWith(SignatureAlgorithm.HS512, SECRET).compact();
        return accessToken;
    }

Security:

入口過濾器

@Configuration
@EnableWebSecurity
@Order(2)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{
    @Value("${server.context-path}")
    String contextPath;
    
    @Override
    protected void configure(HttpSecurity http) throws Exception{
        http.headers().xssProtection().xssProtectionEnabled(true);
        http.csrf().disable().exceptionHandling().authenticationEntryPoint(myEntryPoint())
        .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        .and().authorizeRequests()
        .antMatchers(HttpMethod.OPTIONS).permitAll().antMatchers("/").permitAll().antMatchers("/images/**")
        .permitAll().antMatchers("/*.html").permitAll().antMatchers("/hello/**").permitAll()
        .antMatchers("/admin/add").permitAll().antMatchers("/admin/encode").permitAll()
        .antMatchers("/admin/delete").permitAll().antMatchers("/category/list").permitAll()
        .antMatchers("/module/list").permitAll().antMatchers("/photo/list").permitAll()
        .antMatchers("/music/list").permitAll().antMatchers("/doc/list").permitAll().antMatchers("/vr/list")
        .permitAll().antMatchers("/video/list").permitAll().antMatchers("/category/list/privateOpen")
        .permitAll().antMatchers("/photo/list/privateOpen").permitAll().antMatchers("/video/list/privateOpen")
        .permitAll().antMatchers("/music/list/privateOpen").permitAll().antMatchers("/doc/list/privateOpen")
        .permitAll().antMatchers("/vr/list/privateOpen").permitAll().antMatchers("/health/**").permitAll()
        .antMatchers("/favicon.ico").permitAll().antMatchers("**/*.html").permitAll().antMatchers("**/*.css")
        .permitAll().antMatchers("**/*.js").permitAll().antMatchers("/", "/*swagger*/**", "/v2/api-docs")
        .permitAll()
        .antMatchers("/v2/api-docs", "/configuration/ui", "/swagger-resources", "/configuration/security",
                "/swagger-ui.html", "/webjars/**")
        .permitAll()
        // .antMatchers(HttpMethod.POST, "/logout").authenticated()
        // 所有 /login 的POST請求 都放行
        .antMatchers(HttpMethod.POST, "/login/**").permitAll()

        .antMatchers(HttpMethod.GET, "/token/**").permitAll()
        // 所有請求需要身份認證
        .anyRequest().authenticated().and()
        // 添加一個過濾器 所有訪問 /login 的請求交給 JWTLoginFilter 來處理 這個類處理所有的JWT相關內容
        .addFilterBefore(new JWTLoginFilter("/login", authenticationManager()),
                UsernamePasswordAuthenticationFilter.class)
        // 添加一個過濾器驗證其他請求的Token是否合法
        .addFilterBefore(new JWTAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 使用自定義身份驗證組件
        auth.authenticationProvider(new CustomAuthenticationProvider());
    }
    @Bean
    AuthenticationEntryPoint myEntryPoint() {
        return new ExampleAuthenticationEntryPoint();
    }

}

JWT認證登錄、鑒權

 

public class JWTLoginFilter extends AbstractAuthenticationProcessingFilter {

    public JWTLoginFilter(String url, AuthenticationManager authManager) {
        super(new AntPathRequestMatcher(url));
        setAuthenticationManager(authManager);
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse res)
            throws AuthenticationException, IOException, ServletException {
        try {
            AccountCredentials creds = new AccountCredentials();
            if (creds == null || StringUtils.isEmpty(creds.getUsername()) || StringUtils.isEmpty(creds.getPassword())) {
                String result = JSONResult.fillResultString(HttpServletResponse.SC_BAD_REQUEST, "請求參數無效", null);
                res.setContentType("application/json;charset=UTF-8");
                res.getWriter().println(result);
            }
            return getAuthenticationManager()
                    .authenticate(new UsernamePasswordAuthenticationToken(creds.getUsername(), creds.getPassword()));
        } catch (JsonMappingException ex) {
            String result = JSONResult.fillResultString(1, "[參數異常]", null);
            res.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
            res.getWriter().println(result);
            res.getWriter().close();
        } catch (Exception e) {
            e.printStackTrace();
            String result = JSONResult.fillResultString(10006, "鑒權失敗,請重新登錄", null);
            res.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
            res.getWriter().println(result);
            res.getWriter().close();
        }
        return null;
    }
    @Override
    protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res, FilterChain chain,
            Authentication auth) throws IOException, ServletException {
       TokenAuthenticationService.addAuthentication(res, auth);//響應返回JWT Token
        clearAuthenticationAttributes(req);

    }
    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest req, HttpServletResponse res,
            AuthenticationException failed) throws IOException, ServletException {
        SecurityContextHolder.clearContext();
        res.setContentType("application/json;charset=UTF-8");
        res.setStatus(HttpServletResponse.SC_OK);
        String result = JSONResult.fillResultString(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Internal Server Error!!", null);
        res.getWriter().println(result);
        res.getWriter().close();
    }
    protected final void clearAuthenticationAttributes(HttpServletRequest req) {
        HttpSession session = req.getSession(false);
        if(session == null){
            return;
        }
        session.removeAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
    }
}

認證用戶名和密碼:

@Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        // 獲取認證的用戶名 & 密碼
        String userName = authentication.getName();
        String passWord = authentication.getCredentials().toString();
        if (loginService == null) {
            loginService = SpringContextUtil.getBean("loginService");
        }
        HttpServletRequest request = ((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest();
        String appId = request.getHeader("appId");
        AccountCredentials user = loginService.login(userName, passWord,appId);
        // 認證邏輯
        if (user != null) {

            // // 這里設置權限和角色
            // ArrayList<GrantedAuthority> authorities = new ArrayList<>();
            // authorities.add( new GrantedAuthorityImpl("ROLE_ADMIN") );
            // // authorities.add( new GrantedAuthorityImpl("AUTH_WRITE") );
            // // 生成令牌
            // Authentication auth = new UsernamePasswordAuthenticationToken(name, password,
            // authorities);
            // 生成令牌
            UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(userName, passWord);
            auth.setDetails(user);
            return auth;
        } else {
             throw new BadCredentialsException("密碼錯誤~");
        }
    }

demo源碼下載鏈接:https://pan.baidu.com/s/1kWYeBUR,密碼:plr4

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM