JWT和Spring Security集成

 

一般狀況下,把API直接暴露出去是風險很大的,css

咱們通常須要對API劃分出必定的權限級別,而後作一個用戶的鑑權,依據鑑權結果給予用戶對應的APIhtml

(一)JWT是什麼,爲何要使用它?

互聯網服務離不開用戶認證。通常流程是下面這樣。java

一、用戶向服務器發送用戶名和密碼。jquery

二、服務器驗證經過後,在當前對話(session)裏面保存相關數據,好比用戶角色、登陸時間等等。web

三、服務器向用戶返回一個 session_id,寫入用戶的 Cookie。ajax

四、用戶隨後的每一次請求,都會經過 Cookie,將 session_id 傳回服務器。spring

五、服務器收到 session_id,找到前期保存的數據,由此得知用戶的身份。數據庫

這種模式的問題在於,擴展性(scaling)很差。單機固然沒有問題,若是是服務器集羣,或者是跨域的服務導向架構,就要求 session 數據共享,每臺服務器都可以讀取 session。express

舉例來講,A 網站和 B 網站是同一家公司的關聯服務。如今要求,用戶只要在其中一個網站登陸,再訪問另外一個網站就會自動登陸,請問怎麼實現?apache

一種解決方案是 session 數據持久化,寫入數據庫或別的持久層。各類服務收到請求後,都向持久層請求數據。這種方案的優勢是架構清晰,缺點是工程量比較大。另外,持久層萬一掛了,就會單點失敗。

另外一種方案是服務器索性不保存 session 數據了,全部數據都保存在客戶端,每次請求都發回服務器。JWT 就是這種方案的一個表明。
(引自:阮一峯的網絡日誌 JSON Web Token 入門教程

JSON Web Token(JWT)是一個很是輕巧的規範。這個規範容許咱們使用JWT在用戶和服務器之間傳遞安全可靠的信息

JWT的結構

JWT包含了使用.分隔的三部分:

  • Header 頭部

  • Payload 負載

  • Signature 簽名

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.用戶取得結果

(二)SpringSecurity

Spring Security 是爲基於Spring的應用程序提供聲明式安全保護的安全性框架。

通常來講,Web 應用的安全性包括用戶認證(Authentication)和用戶受權(Authorization)

兩個部分。用戶認證指的是驗證某個用戶是否爲系統中的合法主體,

也就是說用戶可否訪問該系統。用戶認證通常要求用戶提供用戶名和密碼。

系統經過校驗用戶名和密碼來完成認證過程。

用戶受權指的是驗證某個用戶是否有權限執行某個操做。

在一個系統中,不一樣用戶所具備的權限是不一樣的。

好比對一個文件來講,有的用戶只能進行讀取,

而有的用戶能夠進行修改。通常來講,系統會爲不一樣的用戶分配不一樣的角色,

而每一個角色則對應一系列的權限。

對於上面提到的兩種應用情景,Spring Security 框架都有很好的支持。

(三)如何利用Spring Security和JWT一塊兒來完成API保護

1.導入依賴

2.配置application.properties

 spring.jackson.serialization.indent_output=true  //JSON格式化
 logging.level.org.springframework.security=info    //打印security日誌記錄

3.新增 AuthorityName + Authority + 修改 Admins

 

/**
 * 角色枚舉類
 */
public enum AuthorityName {
    ROLE_ADMIN,ROLE_USER
}

import java.io.Serializable;

public class Authority implements Serializable {
    private Integer id;
    private AuthorityName name;

    public Authority() {
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public AuthorityName getName() {
        return name;
    }

    public void setName(AuthorityName name) {
        this.name = name;
    }
}
import java.io.Serializable;
import java.util.Date;
import java.util.List;

public class Admins implements Serializable {
    private Integer aid;
    private String  aname;
    private String pwd;
    private Integer aexist;
    private Integer state;
    private Integer doid;
    private String by1;
    private Date lastPasswordResetDate;

    public Date getLastPasswordResetDate() {
        return lastPasswordResetDate;
    }

    public void setLastPasswordResetDate(Date lastPasswordResetDate) {
        this.lastPasswordResetDate = lastPasswordResetDate;
    }

    private List<Authority> authorities;

    public List<Authority> getAuthorities() {
        return authorities;
    }

    public void setAuthorities(List<Authority> authorities) {
        this.authorities = authorities;
    }

    public String getBy1() {
        return by1;
    }

    public void setBy1(String by1) {
        this.by1 = by1;
    }

    public Integer getDoid() {
        return doid;
    }

    public void setDoid(Integer doid) {
        this.doid = doid;
    }

    public Integer getState() {
        return state;
    }

    public void setState(Integer state) {
        this.state = state;
    }

    public Integer getAexist() {
        return aexist;
    }

    public void setAexist(Integer aexist) {
        this.aexist = aexist;
    }

    public Integer getAid() {
        return aid;
    }

    public void setAid(Integer aid) {
        this.aid = aid;
    }

    public String getAname() {
        return aname;
    }

    public void setAname(String aname) {
        this.aname = aname;
    }

    public String getPwd() {
        return pwd;
    }

    public void setPwd(String pwd) {
        this.pwd = pwd;
    }
}

4.建立安全服務用戶

JwtUser + JwtUserFactory + JwtUserDetailsServiceImpl + JwtAuthenticationResponse

JwtUSer須要實現UserDetails接口,用戶實體即爲Spring Security所使用的用戶

/**
 * 安全服務的用戶
 *  須要實現UserDetails接口,用戶實體即爲Spring Security所使用的用戶
 */
public class JwtUser implements UserDetails {

    private final   Integer id;
    private final  Integer state;
    private final String username;
    private final String password;
    private final String email;
    private final  Collection<? extends GrantedAuthority> authorities;
    private final boolean enabled;
    private final Date lastPasswordResetDate;


    public JwtUser(Integer id, Integer state, String username, String password, String email, Collection<? extends GrantedAuthority> authorities, boolean enabled, Date lastPasswordResetDate) {
        this.id = id;
        this.state = state;
        this.username = username;
        this.password = password;
        this.email = email;
        this.authorities = authorities;
        this.enabled = enabled;
        this.lastPasswordResetDate = lastPasswordResetDate;
    }

    public Integer getState() {
        return state;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

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

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



    @JsonIgnore
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @JsonIgnore
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @JsonIgnore
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return this.enabled;
    }

    @JsonIgnore
    public Integer getId() {
        return id;
    }

    public String getEmail() {
        return email;
    }

    @JsonIgnore
    public Date getLastPasswordResetDate() {
        return lastPasswordResetDate;
    }
}
public final  class JwtUserFactory {

    private JwtUserFactory() {
    }

    public static JwtUser create(Admins user){
         return new JwtUser(
                 user.getAid(),
                 user.getState(),
                 user.getAname(),
                 user.getPwd(),
                 user.getEmail(),
                 mapToGrandAuthroties(user.getAuthorities()),
                 user.getAexist()==1?true:false,
                 user.getLastPasswordResetDate()
         );
    }

    private static List<GrantedAuthority> mapToGrandAuthroties(List<Authority> authorities) {
        return authorities.stream()
                .map(authority -> new SimpleGrantedAuthority(authority.getName().name()))
                .collect(Collectors.toList());


    }

}
@Service
public class JwtUserDetailServiceImpl implements UserDetailsService {

    @Autowired
    private AdminsMapper adminsMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Admins admins = this.adminsMapper.findByUsername(username);
        if(admins==null){
            throw  new UsernameNotFoundException("No User found with UserName :"+username);
        }else{
          return JwtUserFactory.create(admins);
        }
    }
}
public class JwtAuthenticationResponse implements Serializable {
  private static final long serialVersionUID = 4784951536404964122L;
  private final String token;

  public JwtAuthenticationResponse(String token) {
    this.token = token;
  }

  public String getToken() {
    return this.token;
  }
}

配置 application.properties 支持 mybatis 映射文件 xml

mybatis.mapper-locations=classpath:mybatis/mapper/*.xml

5.建立讓Spring控制的安全配置類:WebSecurityConfig

/**
 * 安全配置類
 */
@SuppressWarnings("SpringJavaAutowiringInspection")
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

  @Autowired
  private JwtAuthenticationEntryPoint unauthorizedHandler;

  @Autowired
  private UserDetailsService userDetailsService;


  @Bean
  @Override
  public AuthenticationManager authenticationManagerBean() throws Exception {
    return super.authenticationManagerBean();
  }

  @Autowired
  public void configureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
    authenticationManagerBuilder
            // 設置 UserDetailsService
            .userDetailsService(this.userDetailsService)
            // 使用 BCrypt 進行密碼的 hash
            .passwordEncoder(passwordEncoder());
  }

  /**
   * 裝載 BCrypt 密碼編碼器
   *
   * @return
   */
  @Bean
  public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
  }

  @Bean
  public JwtAuthenticationTokenFilter authenticationTokenFilterBean() throws Exception {
    return new JwtAuthenticationTokenFilter();
  }

  /**
   * token請求受權
   *
   * @param httpSecurity
   * @throws Exception
   */
  @Override
  protected void configure(HttpSecurity httpSecurity) throws Exception {
    httpSecurity
            // we don't need CSRF because our token is invulnerable
            .csrf().disable()
            .cors().and() // 跨域

            .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()

            // don't create session
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()

            .authorizeRequests()
            //.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()

            // allow anonymous resource requests
            .antMatchers(
                    HttpMethod.GET,
                    "/",
                    "/*.html",
                    "/favicon.ico",
                    "/**/*.html",
                    "/**/*.css",
                    "/**/*.js"
            ).permitAll()

            // Un-secure 登陸 驗證碼
            .antMatchers(
                    "/api/auth/**",
                    "/api/verifyCode/**",
                    "/api/global_json"
            ).permitAll()
            // secure other api
            .anyRequest().authenticated();

    // Custom JWT based security filter
    // 將token驗證添加在密碼驗證前面
    httpSecurity
            .addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);

    // disable page caching
    httpSecurity
            .headers()
            .cacheControl();
  }
}

6.在 XxxController 加一個修飾符 @PreAuthorize("hasRole('ADMIN')") 表示這個資源只能被擁有 ADMIN 角色的用戶訪問

@RequestMapping(value = "/protectedadmin", method = RequestMethod.GET)
  @PreAuthorize("hasRole('ADMIN')")
  public ResponseEntity<?> getProtectedAdmin() {
    return ResponseEntity.ok("Greetings from admin protected method!");
  }

  @RequestMapping(value = "/protecteduser", method = RequestMethod.GET)
  @PreAuthorize("hasRole('USER')")
  public ResponseEntity<?> getProtectedUser() {
    return ResponseEntity.ok("Greetings from user protected method!");
  }

 

最後,除了 /api/auth, /api/verifycode, /api/global_json 外請求其餘的路徑
訪問拋異常: org.springframework.security.access.AccessDeniedException: Access is denied


集成 JWT 和 Spring Security,完成鑑權登陸,獲取Token

1.pom.xml中新增依賴 jjwt 依賴

  <!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
  <dependency>
      <groupId>io.jsonwebtoken</groupId>
      <artifactId>jjwt</artifactId>
      <version>0.9.0</version>
  </dependency>

  <!-- https://mvnrepository.com/artifact/com.google.code.findbugs/findbugs -->
  <dependency>
      <groupId>com.google.code.findbugs</groupId>
      <artifactId>findbugs</artifactId>
      <version>3.0.1</version>
  </dependency>

2.application.properties 配置 JWT

3.新建一個filter: JwtAuthenticationTokenFilter :用來驗證令牌的是否合法

JwtAuthenticationEntryPoint(替代默認彈出登陸頁面,返回錯誤信息

+ JwtAuthenticationRequest (登陸信息封裝類)

+JwtTokenUtil(用於生成令牌,驗證等等一些操做)

package com.wutongshu.springboot.security.filter;

import com.wutongshu.springboot.security.JwtTokenUtil;
import io.jsonwebtoken.ExpiredJwtException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * Jwt 過濾器
 */
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

  private final Log logger = LogFactory.getLog(this.getClass());

  @Autowired
  private UserDetailsService userDetailsService;

    @Value("${jwt.header}")
    private String tokenHeader;

    @Value("${jwt.tokenHead}")
    private String tokenHead;

    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
         final String requestHeader  =  request.getHeader(this.tokenHeader);
         String authToken = null;
         String username = null;
         logger.info(requestHeader);
         //當前請求中包含令牌
         if(requestHeader!=null && requestHeader.startsWith(this.tokenHead)){
             authToken = requestHeader.substring(tokenHead.length());
             try {
                 //根據令牌信息獲取用戶名
                  username = jwtTokenUtil.getUsernameFromToken(authToken);
             }catch (IllegalArgumentException e){
                 logger.error("an error occured during getting username from the token ",e);
             }catch (ExpiredJwtException e){
                 logger.error("the token is Expried and not invalid anymore",e);
             }

         }else{
             logger.error("couldn't find Beared String,will ignore the request");
         }
         logger.info("checking Authentication with username : " + username);
         //
         if(username!=null && SecurityContextHolder.getContext().getAuthentication()==null){
            UserDetails userDetails =  userDetailsService.loadUserByUsername(username
              );
            if(jwtTokenUtil.validateToken(authToken,userDetails)){
                UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails,null,userDetails.getAuthorities());
                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                logger.info("authorication user: "+username+", setting security context");
                 SecurityContextHolder.getContext().setAuthentication(authentication);
            }
         }
         chain.doFilter(request,response);
    }
}
package com.wutongshu.springboot.security;

import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.Serializable;

/**
 * 禁止彈出登陸頁面,返回錯誤信息
 */
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint, Serializable {

    private static final long serialVersionUID = -8970718410437077606L;

    @Override
    public void commence(HttpServletRequest request,
                         HttpServletResponse response,
                         AuthenticationException authException) throws IOException {
        // This is invoked when user tries to access a secured REST resource without supplying any credentials
        // We should just send a 401 Unauthorized response because there is no 'login page' to redirect to
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
    }
}
package com.wutongshu.springboot.security;

import java.io.Serializable;

public class JwtAuthenticationRequest implements Serializable {

    private String username;
    private String password;

    public JwtAuthenticationRequest() {
    }

    public JwtAuthenticationRequest(String username, String password) {
        this.username = username;
        this.password = password;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}
package com.wutongshu.springboot.security;

import java.io.Serializable;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;


import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Clock;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.impl.DefaultClock;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

/**
 * 工具類
 *
 */
@Component
public class JwtTokenUtil implements Serializable {
  private static final long serialVersionUID = -3301605591108950415L;
  @SuppressFBWarnings(value = "SE_BAD_FIELD", justification = "It's okay here")
  private Clock clock = DefaultClock.INSTANCE;

  //從application.properties中獲取jwt.secret的值,注入到Secret中
  @Value("${jwt.secret}")
  private String secret;

  @Value("${jwt.expiration}")
  private Long expiration;

  //根據token獲取username
  public String getUsernameFromToken(String token) {
    return getClaimFromToken(token, Claims::getSubject);
  }

  public Date getIssuedAtDateFromToken(String token) {
    return getClaimFromToken(token, Claims::getIssuedAt);
  }

  public Date getExpirationDateFromToken(String token) {
    return getClaimFromToken(token, Claims::getExpiration);
  }

  public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
    final Claims claims = getAllClaimsFromToken(token);
    return claimsResolver.apply(claims);
  }

  private Claims getAllClaimsFromToken(String token) {
    return Jwts.parser()
            .setSigningKey(secret)
            .parseClaimsJws(token)
            .getBody();
  }

  private Boolean isTokenExpired(String token) {
    final Date expiration = getExpirationDateFromToken(token);
    return expiration.before(clock.now());
  }

  private Boolean isCreatedBeforeLastPasswordReset(Date created, Date lastPasswordReset) {
    return (lastPasswordReset != null && created.before(lastPasswordReset));
  }

  private Boolean ignoreTokenExpiration(String token) {
    // here you specify tokens, for that the expiration is ignored
    return false;
  }

  public String generateToken(UserDetails userDetails) {
    Map<String, Object> claims = new HashMap<>();
    return doGenerateToken(claims, userDetails.getUsername());
  }

  private String doGenerateToken(Map<String, Object> claims, String subject) {
    final Date createdDate = clock.now();
    final Date expirationDate = calculateExpirationDate(createdDate);

    return Jwts.builder()
            .setClaims(claims)
            .setSubject(subject)
            .setIssuedAt(createdDate)
            .setExpiration(expirationDate)
            .signWith(SignatureAlgorithm.HS512, secret)
            .compact();
  }

  public Boolean canTokenBeRefreshed(String token, Date lastPasswordReset) {
    final Date created = getIssuedAtDateFromToken(token);
    return !isCreatedBeforeLastPasswordReset(created, lastPasswordReset)
            && (!isTokenExpired(token) || ignoreTokenExpiration(token));
  }

  public String refreshToken(String token) {
    final Date createdDate = clock.now();
    final Date expirationDate = calculateExpirationDate(createdDate);

    final Claims claims = getAllClaimsFromToken(token);
    claims.setIssuedAt(createdDate);
    claims.setExpiration(expirationDate);

    return Jwts.builder()
            .setClaims(claims)
            .signWith(SignatureAlgorithm.HS512, secret)
            .compact();
  }

  public Boolean validateToken(String token, UserDetails userDetails) {
    JwtUser user = (JwtUser) userDetails;
    final String username = getUsernameFromToken(token);
    final Date created = getIssuedAtDateFromToken(token);
    return (
            username.equals(user.getUsername())
                    && !isTokenExpired(token)
                    && !isCreatedBeforeLastPasswordReset(created, user.getLastPasswordResetDate())
    );
  }

  private Date calculateExpirationDate(Date createdDate) {
    return new Date(createdDate.getTime() + expiration * 1000);
  }
}

4.在 WebSecurityConfig 中注入這個filter, 而且配置到 HttpSecurity 中

package com.wutongshu.springboot.security.config;


import com.wutongshu.springboot.security.JwtAuthenticationEntryPoint;
import com.wutongshu.springboot.security.filter.JwtAuthenticationTokenFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

/**
 * 安全配置類
 *
 *
 */
@SuppressWarnings("SpringJavaAutowiringInspection")
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

  @Autowired
  private JwtAuthenticationEntryPoint unauthorizedHandler;

  @Autowired
  private UserDetailsService userDetailsService;


  @Bean
  @Override
  public AuthenticationManager authenticationManagerBean() throws Exception {
    return super.authenticationManagerBean();
  }

  @Autowired
  public void configureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
    authenticationManagerBuilder
            // 設置 UserDetailsService
            .userDetailsService(this.userDetailsService)
            // 使用 BCrypt 進行密碼的 hash
            .passwordEncoder(passwordEncoder());
  }

  /**
   * 裝載 BCrypt 密碼編碼器
   *
   * @return
   */
  @Bean
  public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
  }

  @Bean
  public JwtAuthenticationTokenFilter authenticationTokenFilterBean() throws Exception {
    return new JwtAuthenticationTokenFilter();
  }

  /**
   * token請求受權
   *
   * @param httpSecurity
   * @throws Exception
   */
  @Override
  protected void configure(HttpSecurity httpSecurity) throws Exception {
    httpSecurity
            // we don't need CSRF because our token is invulnerable
            .csrf().disable()
            .cors().and() // 跨域

            .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()

            // don't create session
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()

            .authorizeRequests()
            //.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()

            // allow anonymous resource requests
            .antMatchers(
                    HttpMethod.GET,
                    "/",
                    "/*.html",
                    "/favicon.ico",
                    "/**/*.html",
                    "/**/*.css",
                    "/**/*.js"
            ).permitAll()

            // Un-secure 登陸 驗證碼
            .antMatchers(
                    "/api/auth/**",
                    "/alogin"
            ).permitAll()
            // secure other api
            .anyRequest().authenticated();

    // Custom JWT based security filter
    // 將token驗證添加在密碼驗證前面
    httpSecurity
            .addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);

    // disable page caching
    httpSecurity
            .headers()
            .cacheControl();
  }
}

最後:

完成鑑權(登陸),註冊和更新token的功能
.AuthenticationRestController + MethodProtectedRestController + UserRestController

package com.wutongshu.springboot.security.controller;



import com.wutongshu.springboot.security.JwtAuthenticationRequest;
import com.wutongshu.springboot.security.JwtAuthenticationResponse;
import com.wutongshu.springboot.security.JwtTokenUtil;
import com.wutongshu.springboot.security.JwtUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;

@RestController
@RequestMapping("/api")
public class AuthenticationRestController {

  @Value("${jwt.header}")
  private String tokenHeader;

  @Autowired
  private AuthenticationManager authenticationManager;

  @Autowired
  private JwtTokenUtil jwtTokenUtil;

  @Autowired
  private UserDetailsService userDetailsService;

  @RequestMapping(value = "${jwt.route.authentication.path}", method = RequestMethod.POST)
  public ResponseEntity<?> createAuthenticationToken(@RequestBody JwtAuthenticationRequest authenticationRequest) throws AuthenticationException {
    UsernamePasswordAuthenticationToken upToken = new UsernamePasswordAuthenticationToken(
            authenticationRequest.getUsername(),
            authenticationRequest.getPassword()
    );
    // Perform the security
    final Authentication authentication = authenticationManager.authenticate(upToken);
    SecurityContextHolder.getContext().setAuthentication(authentication);

    // Reload password post-security so we can generate token
    final UserDetails userDetails = userDetailsService.loadUserByUsername(authenticationRequest.getUsername());
    final String token = jwtTokenUtil.generateToken(userDetails);

    // Return the token
    return ResponseEntity.ok(new JwtAuthenticationResponse(token));
  }

  @RequestMapping(value = "${jwt.route.authentication.refresh}", method = RequestMethod.GET)
  public ResponseEntity<?> refreshAndGetAuthenticationToken(HttpServletRequest request) {
    String authToken = request.getHeader(tokenHeader);
    final String token = authToken.substring(7);
    String username = jwtTokenUtil.getUsernameFromToken(token);
    JwtUser user = (JwtUser) userDetailsService.loadUserByUsername(username);

    if (jwtTokenUtil.canTokenBeRefreshed(token, user.getLastPasswordResetDate())) {
      String refreshedToken = jwtTokenUtil.refreshToken(token);
      return ResponseEntity.ok(new JwtAuthenticationResponse(refreshedToken));
    } else {
      return ResponseEntity.badRequest().body(null);
    }
  }
}
package com.wutongshu.springboot.security.controller;

import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;


@RestController
@RequestMapping("/api")
public class MethodProtectedRestController {

  /**
   * This is an example of some different kinds of granular restriction for endpoints. You can use the built-in SPEL expressions
   * in @PreAuthorize such as 'hasRole()' to determine if a user has access. Remember that the hasRole expression assumes a
   * 'ROLE_' prefix on all role names. So 'ADMIN' here is actually stored as 'ROLE_ADMIN' in database!
   **/
  @RequestMapping(value = "/protectedadmin", method = RequestMethod.GET)
  @PreAuthorize("hasRole('ADMIN')")
  public ResponseEntity<?> getProtectedAdmin() {
    return ResponseEntity.ok("Greetings from admin protected method!");
  }

  @RequestMapping(value = "/protecteduser", method = RequestMethod.GET)
  @PreAuthorize("hasRole('USER')")
  public ResponseEntity<?> getProtectedUser() {
    return ResponseEntity.ok("Greetings from user protected method!");
  }
}
package com.wutongshu.springboot.security.controller;


import com.wutongshu.springboot.security.JwtTokenUtil;
import com.wutongshu.springboot.security.JwtUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;


@RestController
@RequestMapping("/api")
public class UserRestController {

  @Value("${jwt.header}")
  private String tokenHeader;

  @Autowired
  private JwtTokenUtil jwtTokenUtil;

  @Autowired
  private UserDetailsService userDetailsService;

  /**
   * 獲取受權的用戶信息
   *
   * @param request
   * @return
   */
  @RequestMapping(value = "/user", method = RequestMethod.GET)
  public JwtUser getAuthenticatedUser(HttpServletRequest request) {
    String token = request.getHeader(tokenHeader).substring(7);
    String username = jwtTokenUtil.getUsernameFromToken(token);
    JwtUser user = (JwtUser) userDetailsService.loadUserByUsername(username);
    return user;
  }

}

前臺頁面:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8"></meta>
  <title>JWT Spring Security Demo</title>
  <link rel="shortcut icon" href="favicon.ico" type="image/x-icon"></link>
  <!-- Latest compiled and minified CSS -->
  <link rel="stylesheet" href="../css/bootstrap.min.css"></link>
</head>
<body>
<div class="container">
  <h1>JWT Spring Security Demo</h1>

  <div class="alert alert-danger" id="notLoggedIn">Not logged in!</div>

  <div class="row">
    <div class="col-md-6">
      <div class="panel panel-default" id="login">
        <div class="panel-heading">
          <h3 class="panel-title">Login</h3>
        </div>
        <div class="panel-body">
          <form id="loginForm">
            <div class="form-group">
              <input type="text" class="form-control" id="exampleInputEmail1" placeholder="username"
                     required name="username"/>
            </div>
            <div class="form-group">
              <input type="password" class="form-control" id="exampleInputPassword1"
                     placeholder="password" required name="password">
            </div>
            <div class="well">
              Try one of the following logins
              <ul>
                <li>admin & admin</li>
                <li>user & password</li>
                <li>disabled & password</li>
              </ul>
            </div>
            <button type="submit" class="btn btn-default">login</button>
          </form>
        </div>
      </div>

      <div id="userInfo">
        <div class="panel panel-default">
          <div class="panel-heading">
            <h3 class="panel-title">Authenticated user</h3>
          </div>
          <div class="panel-body">
            <div id="userInfoBody"></div>
            <button type="button" class="btn btn-default" id="logoutButton">logout</button>
          </div>
        </div>
      </div>
    </div>

    <div class="col-md-6">
      <div class="btn-group" role="group" aria-label="..." style="margin-bottom: 16px;">
        <button type="button" class="btn btn-default" id="exampleServiceBtn">call user protected service</button>
        <button type="button" class="btn btn-default" id="adminServiceBtn">call admin protected service</button>
      </div>
      <div class="panel panel-default">
        <div class="panel-heading">
          <h3 class="panel-title">Response:</h3>
        </div>
        <div class="panel-body">
          <pre id="response"></pre>
        </div>
      </div>
    </div>
  </div>

  <div class="row">
    <div id="loggedIn" class="col-md-6">
      <div class="panel panel-default">
        <div class="panel-heading">
          <h3 class="panel-title">Token information</h3>
        </div>
        <div class="panel-body" id="loggedInBody"></div>
      </div>
    </div>
  </div>
</div>

<div class="modal fade" tabindex="-1" role="dialog" id="loginErrorModal">
  <div class="modal-dialog">
    <div class="modal-content">
      <div class="modal-header">
        <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span
                aria-hidden="true">&times;</span></button>
        <h4 class="modal-title">Login unsuccessful</h4>
      </div>
      <div class="modal-body"></div>
      <div class="modal-footer">
        <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
      </div>
    </div><!-- /.modal-content -->
  </div><!-- /.modal-dialog -->
</div><!-- /.modal -->

<script src="../js/jquery-3.3.1.min.js"></script>
<!-- Latest compiled and minified JavaScript -->
<script src="../js/bootstrap.min.js"></script>
<script src="../js/jwt-decode.min.js"></script>
<script src="client.js"></script>
</body>
</html>

client.js:

$(function () {
  // VARIABLES =============================================================
  var TOKEN_KEY = "jwtToken"
  var $notLoggedIn = $("#notLoggedIn");
  var $loggedIn = $("#loggedIn").hide();
  var $loggedInBody = $("#loggedInBody");
  var $response = $("#response");
  var $login = $("#login");
  var $userInfo = $("#userInfo").hide();

  // FUNCTIONS =============================================================
  function getJwtToken() {
    return localStorage.getItem(TOKEN_KEY);
  }

  function setJwtToken(token) {
    localStorage.setItem(TOKEN_KEY, token);
  }

  function removeJwtToken() {
    localStorage.removeItem(TOKEN_KEY);
  }

  function doLogin(loginData) {
    $.ajax({
      url: "http://127.0.0.1:8087/test/api/auth",
      type: "POST",
      data: JSON.stringify(loginData),
      contentType: "application/json; charset=utf-8",
      dataType: "json",
      success: function (data, textStatus, jqXHR) {
        setJwtToken(data.token);
        $login.hide();
        $notLoggedIn.hide();
        showTokenInformation();
        showUserInformation();
      },
      error: function (jqXHR, textStatus, errorThrown) {
        if (jqXHR.status === 401) {
          $('#loginErrorModal')
              .modal("show")
              .find(".modal-body")
              .empty()
              .html("<p>Spring exception:<br>" + jqXHR.responseJSON.exception + "</p>");
        } else {
          throw new Error("an unexpected error occured: " + errorThrown);
        }
      }
    });
  }

  function doLogout() {
    removeJwtToken();
    $login.show();
    $userInfo
        .hide()
        .find("#userInfoBody").empty();
    $loggedIn.hide();
    $loggedInBody.empty();
    $notLoggedIn.show();
  }

  function createAuthorizationTokenHeader() {
    var token = getJwtToken();
    if (token) {
      return {"Authorization": "Bearer " + token};
    } else {
      return {};
    }
  }

  function showUserInformation() {
    $.ajax({
      url: "http://127.0.0.1:8087/test/api/user",
      type: "GET",
      contentType: "application/json; charset=utf-8",
      dataType: "json",
      headers: createAuthorizationTokenHeader(),
      success: function (data, textStatus, jqXHR) {
        var $userInfoBody = $userInfo.find("#userInfoBody");

        $userInfoBody.append($("<div>").text("Username: " + data.username));
        $userInfoBody.append($("<div>").text("Email: " + data.email));

        var $authorityList = $("<ul>");
        data.authorities.forEach(function (authorityItem) {
          $authorityList.append($("<li>").text(authorityItem.authority));
        });
        var $authorities = $("<div>").text("Authorities:");
        $authorities.append($authorityList);

        $userInfoBody.append($authorities);
        $userInfo.show();
      }
    });
  }

  function showTokenInformation() {
    var jwtToken = getJwtToken();
    var decodedToken = jwt_decode(jwtToken);

    $loggedInBody.append($("<h4>").text("Token"));
    $loggedInBody.append($("<div>").text(jwtToken).css("word-break", "break-all"));
    $loggedInBody.append($("<h4>").text("Token claims"));

    var $table = $("<table>")
        .addClass("table table-striped");
    appendKeyValue($table, "sub", decodedToken.sub);
    appendKeyValue($table, "aud", decodedToken.aud);
    appendKeyValue($table, "iat", decodedToken.iat);
    appendKeyValue($table, "exp", decodedToken.exp);

    $loggedInBody.append($table);

    $loggedIn.show();
  }

  function appendKeyValue($table, key, value) {
    var $row = $("<tr>")
        .append($("<td>").text(key))
        .append($("<td>").text(value));
    $table.append($row);
  }

  function showResponse(statusCode, message) {
    $response
        .empty()
        .text("status code: " + statusCode + "\n-------------------------\n" + message);
  }

  // REGISTER EVENT LISTENERS =============================================================
  $("#loginForm").submit(function (event) {
    event.preventDefault();

    var $form = $(this);
    var formData = {
      username: $form.find('input[name="username"]').val(),
      password: $form.find('input[name="password"]').val()
    };

    doLogin(formData);
  });

  $("#logoutButton").click(doLogout);

  $("#exampleServiceBtn").click(function () {
    $.ajax({
      url: "http://127.0.0.1:8087/test/api/protecteduser",
      type: "GET",
      contentType: "application/json; charset=utf-8",
      headers: createAuthorizationTokenHeader(),
      success: function (data, textStatus, jqXHR) {
        showResponse(jqXHR.status, data);
      },
      error: function (jqXHR, textStatus, errorThrown) {
        showResponse(jqXHR.status, errorThrown);
      }
    });
  });

  $("#adminServiceBtn").click(function () {
    $.ajax({
      url: "http://127.0.0.1:8087/test/api/protectedadmin",
      type: "GET",
      contentType: "application/json; charset=utf-8",
      headers: createAuthorizationTokenHeader(),
      success: function (data, textStatus, jqXHR) {
        showResponse(jqXHR.status, data);
      },
      error: function (jqXHR, textStatus, errorThrown) {
        showResponse(jqXHR.status, errorThrown);
      }
    });
  });

  $loggedIn.click(function () {
    $loggedIn
        .toggleClass("text-hidden")
        .toggleClass("text-shown");
  });

  // INITIAL CALLS =============================================================
  if (getJwtToken()) {
    $login.hide();
    $notLoggedIn.hide();
    showTokenInformation();
    showUserInformation();
  }
});

效果圖:

當咱們用admin的用戶登陸時:

當咱們用USER權限的用戶登陸時

 

 logout方法裏清空token

其餘注意事項:1.每次請求都必須攜帶頭部信息,並且必須是JSON對象類型,也就是

createAuthorizationTokenHeader()方法

2.WebSecurityConfig配置類裏的限制路徑並不須要拼接properties裏的路徑,也就是說方法裏實際是controller層的路徑

相關文章
相關標籤/搜索