Spring Boot 2.X 實戰--Spring Security (Token)登錄和註冊

博客主頁:https://me.csdn.net/u010974701

源代碼倉庫:https://github.com/zhshuixian/learn-spring-boot-2

在上一節《Spring Boot 2.X 實戰–Spring Security 登錄和註冊》中,我們主要整合 Spring Security,實現了用戶的註冊、登錄和權限控制。

在這一節中,我們將實現基於 Token 令牌的 Web 應用安全訪問控制。Token 令牌是一種無狀態的認證方式,它不會像 Session 那樣在服務器端保留用戶認證方面的信息,非常適合 RESTful API。

對於純 Token 認證的 Web 應用,大概思路是:在上一個項目基礎上,關閉 Spring Security 自帶的 Session,允許跨域請求;增加一個 Token 攔截器,攔截所有請求並驗證 Token 令牌是否有效。

如果想要實現 Session + Token,我的想法是,增加 Session、Token 攔截器,同樣攔截所有的請求。帶 Cookie 的請求用 Session 認證授權;不帶 Cookie 的交給 Token 攔截器認證驗證授權。(這個想法暫沒有實踐過,嘻嘻,可以的話歡迎留言告訴小先)。

本小節中,不使用 MVC 模式,使用 MySQL,MyBatis 構建 RESTful API 實現。

1)什麼是 Token

1.1)服務器是怎麼知道用戶有沒有登錄的

我們知道,HTTP 協議本身是不包括狀態的,每一次客戶端——服務器 HTTP 通信結束後就斷開了,因此 HTTP 不會記住這個登錄用戶是誰。

爲了解決每一次請求都要帶上用戶名、用戶密碼來進行用戶身份驗證的情況,一般有以下兩種方法,兩種方法各有優劣,根據項目實際情況選用即可。

基於 Session 的認證方法

1、用戶登錄後,在服務端生成用戶的登錄信息(Session),並把登錄信息的 ID(Session_ID)返回給客戶端。客戶端一般通過 Cookie 保存。

2、客戶端每一次請求只需要帶上登錄信息 ID(Session_ID)到服務器上找有沒有對應的登錄信息(Session)即可。

3、如果找到了登錄信息(Session)則表示通過驗證。

對於單個服務的應用來說沒有什麼問題,如果要擴展爲分佈式應用。例如,一個公司有 A、B 兩個服務,需要要實現用戶在任意一個服務登錄後就可以訪問兩個服務。實現思路是把 Session 持久化,比如寫入數據庫或者 Redis 緩存,驗證的時候去數據庫或者 Redis 緩存找 Session,定時清理數據庫或者設置 Redis 過期時間。

基於 Token 的認證方法

這種方法,不在服務端保存用戶的登錄信息,用戶 Token 令牌保存在客戶端。客戶端請求帶上 Token 信息,服務端驗證 Token 的有效性即可。

1、用戶輸入用戶名、用戶密碼,驗證之後加密生成 Token

2、客戶端保存 Token,每次請求的時候帶上此 Token

3、服務端根據密鑰驗證 Token 的有效性

對於 A、B 兩個服務如果使用了相同的加密算法和密鑰,那麼任意一個服務生成的 Token 都可以直接在另一個服務使用,即使新增一個 C 服務,只要加密算法和密鑰相同,三個服務的 Token 還是通用的。

對於想用戶更改密碼後使未過期的 Token 失效的需求,可以通過在用戶表新增一個用戶修改信息的時間戳,只要 Token 是在這個時間戳前生成的,直接默認無效;還有一種方法也是增加一個 Redis 緩存,更改用戶信息後刪除該用戶的 Token 緩存,緩存中沒有用戶 Token 直接默認失效。

對於配置單點登錄的方法,我目前的思路還是隻有 Redis 緩存這一個(手動捂臉)

1.2)Token 長什麼樣的?

Token 由三部分構成

  • Header(頭部)
  • Payload(負載)
  • Signature(簽名)

完整的 Token 示例,三部分由小數點 . 分割開:

eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJTcHJpbmdCb290IiwiYXVkIjoiYm9vdCIsImV4cCI6MTU4NjE5MzMwMywiaWF0IjoxNTg1NTg4NTAzLCJyb2xlIjoiQURNSU4ifQ.SdHSoet9BEaMcBrbbwO4_nd88nO7VIuV6IB_Kdw1AFmmPPCxY8CKUoE-QrJmN3RSMAdxLB0GAfDiwFV6zpxVZksXZQAQzxa_bPw0JUj7mZyHzdSR2jNm_oKB_2rnRsfW7caXZVgwtUU2lHoXSLdlgHRqoyIw7AcP5dm-og3ELGgUQxa27mmwvXtRngfvgw1EKoeA_bdwNSbDWu8clyNjd9ftq9_yU3QKFc3NAUVkWTRa8U1_dyOI9B4LMrKrXEQSR8D7UDw-0MDbOZNwzUmxv0h-QER1cw5dxnQsMs2C9TI32x9E68PaNC8PkaAyOkCs55y-W7wyf-K24fzt5nQb4w

需要注意的是,頭部和負載是不加密的,由 Bas64 算法將 JSON 格式的數據編碼而來,因此不能在 Token 存儲密碼以及用戶隱私信息

Header 頭部

{
  "alg": "HS256"
}

頭部一般是聲明 Token 的 Signature(簽名) 的加密算法,經過 Base64 編碼後得到 Token 的第一部分,示例如下:

eyJhbGciOiJIUzI1NiJ9

Payload(負載)

{
  "sub": "Joe"
}

這是 Token 的主體部分,這裏同樣是經過 Base64 編碼後得到 Token 的第二部分,示例如下:

eyJzdWIiOiJKb2UifQ.1KP0SsvENi7Uz1oQc07aXTL7kpQG5jBNIybqr60AlD4

需要注意的是 Payload(負載)不是加密的,請勿在這裏存儲任何私密信息,比如密碼、用戶隱私信息。

常用的設置如下,標準 Claims:

  • setIssuer: Token 簽發者
  • setSubject: Token 主題
  • setAudience: Token 接受者
  • setExpiration: 過期時間
  • setNotBefore: 在什麼日期後才生效
  • setIssuedAt: 簽發時間
  • setId: 唯一的標識符

自定義 Claims:

  • .claim("role", "ADMIN") : 通過此方法自定義了一個名稱爲 role,值爲 ADMIN 的 Claims

Signature(簽名)

簽名部分是對 Token 的 Header 頭部Payload(負載) 通過頭部聲明的加密方式和服務端定義密鑰加密生成的,如果 Header 頭部 和 **Payload(負載)**的數據發生改變,Signature(簽名)隨即發生改變,在密鑰未泄露的情況,其他人無法篡改或者僞造 Token,這也是爲什麼 Token 是安全的。

SdHSoet9BEaMcBrbbwO4_nd88nO7VIuV6IB_Kdw1AFmmPPCxY8CKUoE-QrJmN3RSMAdxLB0GAfDiwFV6zpxVZksXZQAQzxa_bPw0JUj7mZyHzdSR2jNm_oKB_2rnRsfW7caXZVgwtUU2lHoXSLdlgHRqoyIw7AcP5dm-og3ELGgUQxa27mmwvXtRngfvgw1EKoeA_bdwNSbDWu8clyNjd9ftq9_yU3QKFc3NAUVkWTRa8U1_dyOI9B4LMrKrXEQSR8D7UDw-0MDbOZNwzUmxv0h-QER1cw5dxnQsMs2C9TI32x9E68PaNC8PkaAyOkCs55y-W7wyf-K24fzt5nQb4w

JJWT 是用於在 JVM 和 Android 上創建和驗證 JSON Web Token(JWT) 的開源(Apache 2.0)工具包,是基於 JWT、JWS、 JWE、JWK 規範的 Java 實現。支持的加密算法有 HMAC,RSASSA,ECDSA,在開發過程中,必須使用對所選算法而言足夠強的密鑰。這一小節中,將採用 RSA 非對稱加密的方式加密 Token。

2)JJWT 依賴引入和項目配置

新建項目 06-security-token,注意 Spring Boot 的版本要爲 2.1.X 版本,記得勾選 Spring Security,MySQL,MyBatis,Web 依賴。

對於 Maven 項目,同樣是在文章最後面給出對應的依賴配置。

Gradle 項目配置

    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.1.2'
    runtimeOnly 'mysql:mysql-connector-java'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'org.springframework.security:spring-security-test'
    // 添加如下的依賴 https://github.com/jwtk/jjwt
    compile 'io.jsonwebtoken:jjwt-api:0.11.1'
    runtime 'io.jsonwebtoken:jjwt-impl:0.11.1',
            // Uncomment the next line if you want to use RSASSA-PSS (PS256, PS384, PS512) algorithms:
            //'org.bouncycastle:bcprov-jdk15on:1.60',
            // or 'io.jsonwebtoken:jjwt-gson:0.11.1' for gson
            'io.jsonwebtoken:jjwt-jackson:0.11.1'

1.2)項目配置

配置 MySQL 數據庫和 MyBatis 駝峯命名轉換,application.properties

# 數據庫 URL、用戶名、密碼、JDBC Driver更換數據庫只需更改這些信息即可
# MySQL 8 需要指定 serverTimezone 才能連接成功
spring.datasource.url=jdbc:mysql://localhost:3306/spring?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
spring.datasource.password=xiaoxian
spring.datasource.username=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# MyBatis 駝峯命名轉換
mybatis.configuration.map-underscore-to-camel-case=true

添加 @MapperScan

@MapperScan("org.xian.security.mapper")
public class SecurityApplication {}

2)開始使用 JJWT

項目的主要結構:

  • controller 包:API 接口
  • service 包:爲 API 提供接口服務
  • mapper 包:MyBatis Mapper 類
  • entity 包:實體類
  • security 包:Token 攔截驗證、Token 生成、Spring Security 配置

MyResponse :公共 Response 返回消息類:

public class MyResponse implements Serializable {
    private static final long serialVersionUID = -2L;
    private String status;
    private String message;
}

2.1)實體類 Entity 和 Mapper

這裏的表結構和上一節一樣, 用戶表 sys_user

字段 類型 備註
user_id bigint 自增主鍵
username varchar(18) 用戶名,非空唯一
password varchar(60) 密碼,非空
user_role varchar(8) 用戶角色(USER / ADMIN)

這裏用戶角色有 USER / ADMIN ,對於一個用戶可能有多個角色的情況暫不考慮。

SQL

use spring;
create table sys_user
(
    user_id   bigint auto_increment,
    username  varchar(18)  not null unique,
    password  varchar(60) not null,
    user_role varchar(8)   not null,
    constraint sys_user_pk
        primary key (user_id)
);

Entity 實體類:新建 package,名稱爲 entity 。在 entity下新建一個 SysUser 類:

public class SysUser implements Serializable {
    private static final long serialVersionUID = 4522943071576672084L;
    private Long userId;
    private String username;
    private String password;
    private String userRole;
    // 省略 getter setter constructor
}

Mapper 接口類:新建包 mapper,新建 SysUserMapper 類:

// 這裏使用註解的方式
public interface SysUserMapper {
    /** 往 sys_user 插入一條記錄
     * @param sysUser 用戶信息
     */
    @Insert("Insert Into sys_user(username, password,user_role) Values(#{username}, #{password},#{userRole})")
    @Options(useGeneratedKeys = true, keyProperty = "userId")
    void insert(SysUser sysUser);

    /** 根據用戶 Username 查詢用戶信息
     * @param username 用戶名
     * @return 用戶信息
     */
    @Select("Select user_id,username, password,user_role From sys_user Where username=#{username}")
    SysUser selectByUsername(String username);
}

2.2)Token 配置

如以下幾個部分構成

  • RSA 密鑰公鑰工具類
  • Token 生成、驗證工具類
  • Token 攔截器,攔截所有請求並驗證 Token 是否有效
  • 自定義未實現授權訪問錯誤處理類
  • 自定義實現 UserDetailsService 接口
  • Spring Security 配置

在 Security 包下新建 RsaUtils 類,RSA 的公鑰和密鑰的工具類。注意在 JDK 8 中,2048 位的密鑰不受支持。

public class RsaUtils {
    /** PrivateKey * 生成祕鑰 > openssl genrsa -out rsa_private_key.pem 2048 
     * 轉換成PKCS8格式 >openssl pkcs8 -topk8 -inform * PEM -in rsa_private_key.pem -outform PEM -nocrypt 
     * 在終端輸出結果,去掉“-----BEGIN PRIVATE KEY-----” * “-----END PRIVATE KEY-----”
     * @return PrivateKey
     */
    public static PrivateKey getPrivateKey() {
        PrivateKey privateKey = null;
        try {
            String privateKeyStr = "PrivateKey";
            // PKCS8格式的密鑰
            PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKeyStr));
            // RSA 算法
            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
            return keyFactory.generatePrivate(keySpec);
        } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
            e.printStackTrace();
        }
        return privateKey;
    }

    /** PublicKey 根據 祕鑰 生成public key > openssl rsa -in rsa_private_key.pem -out rsa_public_key.pem -pubout
     * @return PublicKey
     */
    public static PublicKey getPublicKey() {
        PublicKey publicKey = null;
        try {
            String publicKeyStr = "public key";
            X509EncodedKeySpec keySpec = new X509EncodedKeySpec(Base64.getDecoder().decode(publicKeyStr));
            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
            return keyFactory.generatePublic(keySpec);
        } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
            e.printStackTrace();
            return null;
        }
    }
}

TokenUtils : 生成和驗證 Token 的工具類。可選的 Token 主體部分是指在驗證 和授權的時候用不上這些信息:

@Component
public class TokenUtils implements Serializable {
    private static final long serialVersionUID = -3L;
    /**
     * Token 有效時長
     */
    private static final Long EXPIRATION = 604800L;

    /** 生成 Token 字符串 必須 setAudience 接收者 setExpiration 過期時間 role 用戶角色
     * @param sysUser 用戶信息
     * @return 生成的Token字符串 or null
     */
    public String createToken(SysUser sysUser) {
        try {
            // Token 的過期時間
            Date expirationDate = new Date(System.currentTimeMillis() + EXPIRATION * 1000);
            // 生成 Token
            String token = Jwts.builder()
                    // 設置 Token 簽發者 可選
                    .setIssuer("SpringBoot")
                    // 根據用戶名設置 Token 的接受者
                    .setAudience(sysUser.getUsername())
                    // 設置過期時間
                    .setExpiration(expirationDate)
                    // 設置 Token 生成時間 可選
                    .setIssuedAt(new Date())
                    // 通過 claim 方法設置一個 key = role,value = userRole 的值
                    .claim("role", sysUser.getUserRole())
                    // 設置加密密鑰和加密算法,注意要用私鑰加密且保證私鑰不泄露
                    .signWith(RsaUtils.getPrivateKey(), SignatureAlgorithm.RS256)
                    .compact();
            return String.format("Bearer %s", token);
        } catch (Exception e) {
            return null;
        }
    }

    /** 驗證 Token ,並獲取到用戶名和用戶權限信息
     * @param token Token 字符串
     * @return sysUser 用戶信息
     */
    public SysUser validationToken(String token) {
        try {
            // 解密 Token,獲取 Claims 主體
            Claims claims = Jwts.parserBuilder()
                    // 設置公鑰解密,以爲私鑰是保密的,因此 Token 只能是自己生成的,如此來驗證 Token
                    .setSigningKey(RsaUtils.getPublicKey())
                    .build().parseClaimsJws(token).getBody();
            assert claims != null;
            // 驗證 Token 有沒有過期 過期時間
            Date expiration = claims.getExpiration();
            // 判斷是否過期 過期時間要在當前日期之後
            if (!expiration.after(new Date())) {
                return null;
            }
            SysUser sysUser = new SysUser();
            sysUser.setUsername(claims.getAudience());
            sysUser.setUserRole(claims.get("role").toString());
            return sysUser;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
}

TokenFilter :Token 攔截器,攔截所有請求並驗證 Token 是否有效,有效則授權通過,無效則由 Spring Security 根據配置攔截無效請求:

@SuppressWarnings("SpringJavaAutowiringInspection")
@Service
public class TokenFilter extends OncePerRequestFilter {
    @Resource
    TokenUtils tokenUtils;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        // 存儲 Token 的 Headers Key與 Value,默認是 Authorization
        final String authorizationKey = "Authorization";
        String authorizationValue;
        try {
            authorizationValue = request.getHeader(authorizationKey);
        } catch (Exception e) {
            authorizationValue = null;
        }
        // Token 開頭部分 默認 Bearer 開頭
        String bearer = "Bearer ";
        if (authorizationValue != null && authorizationValue.startsWith(bearer)) {
            // token
            String token = authorizationValue.substring(bearer.length());
            SysUser sysUser = tokenUtils.validationToken(token);
            if (sysUser != null) {
                // Spring Security 角色名稱默認使用 "ROLE_" 開頭
                // authorities.add 可以增加多個用戶角色,對於一個用戶有多種角色的系統來說,
                // 可以通過增加用戶角色表、用戶--角色映射表,存儲多個用戶角色信息
                List<SimpleGrantedAuthority> authorities = new ArrayList<>();
                authorities.add(new SimpleGrantedAuthority("ROLE_" + sysUser.getUserRole()));
                // 傳入用戶名、用戶密碼、用戶角色。 這裏的密碼隨便寫的,用不上
                UserDetails userDetails = new User(sysUser.getUsername(), "password", authorities);
                UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
                        userDetails, null, userDetails.getAuthorities());
                authentication.setDetails(userDetails.getUsername());
                // 授權
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        }
        filterChain.doFilter(request, response);
    }
}

ErrorAuthenticationEntryPoint:錯誤消息類,未授權訪問通過此類返回 401 未授權訪問信息。自定義實現 Spring Security 的默認未授權訪問處理接口 AuthenticationEntryPoint:

@Component
public class ErrorAuthenticationEntryPoint implements AuthenticationEntryPoint, Serializable {
    private static final long serialVersionUID = 5200068540912465653L;
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response,
                         AuthenticationException authException) throws IOException {
        // 設置 Json 格式返回
        response.setContentType("application/json;charset=UTF-8");
        // 設置 HTTP 狀態碼爲 401
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        // PrintWriter 輸出 Response 返回信息
        PrintWriter writer = response.getWriter();
        ObjectMapper mapper = new ObjectMapper();
        MyResponse myResponse = new MyResponse("error", "非授權訪問");
        // 將對象輸出爲 JSON 格式。可以通過重寫 MyResponse 的 toString() ,直接通過 myResponse.toString() 即可
        writer.write(mapper.writeValueAsString(myResponse));
    }
}

UserDetailsServiceImpl :自定義實現 UserDetailsService,通過重寫UserDetailsService接口的loadUserByUsername 方法,給 Spring Security 傳入用戶名、用戶密碼、用戶角色。

@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    @Resource
    private SysUserMapper sysUserMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        SysUser sysUser = sysUserMapper.selectByUsername(username);
        if (sysUser == null ) {
            throw new UsernameNotFoundException(username);
        }
        List<SimpleGrantedAuthority> authorities = new ArrayList<>();
        // Spring Security 角色名稱默認使用 "ROLE_" 開頭
        // authorities.add 可以增加多個用戶角色,對於一個用戶有多種角色的系統來說,
        // 可以通過增加用戶角色表、用戶--角色映射表,存儲多個用戶角色信息
        authorities.add(new SimpleGrantedAuthority("ROLE_" + sysUser.getUserRole()));
        // 給 Spring Security 傳入用戶名、用戶密碼、用戶角色。
        return new User(sysUser.getUsername(), sysUser.getPassword(), authorities);
    }
}

SpringSecurityConfig :Spring Security 配置,配置密碼存儲加密算法,添加攔截器,關閉 Session 管理器,允許跨域訪問,允許登錄和註冊的 API 無授權訪問。

@SuppressWarnings("SpringJavaAutowiringInspection")
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {

    @Resource
    private UserDetailsServiceImpl userDetailsService;

    @Resource
    private ErrorAuthenticationEntryPoint errorAuthenticationEntryPoint;

    @Resource
    private TokenFilter tokenFilter;

    @Autowired
    public void configureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
        // 使用 BCryptPasswordEncoder 驗證密碼
        authenticationManagerBuilder.userDetailsService(this.userDetailsService).passwordEncoder(passwordEncoder());
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        // BCrypt 密碼
        return new BCryptPasswordEncoder();
    }

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

    @Override
    public void configure(HttpSecurity httpSecurity) throws Exception {
        // 配置 CSRF 關閉,允許跨域訪問
        httpSecurity.csrf().disable();
        // 指定錯誤未授權訪問的處理類
        httpSecurity.exceptionHandling().authenticationEntryPoint(errorAuthenticationEntryPoint);
        // 關閉 Session
        httpSecurity.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        // 允許 登錄 註冊的 api 的無授權訪問,其他需要授權訪問
        httpSecurity.authorizeRequests()
                .antMatchers("/api/user/login", "/api/user/register")
                .permitAll().anyRequest().authenticated();
        // 添加攔截器
        httpSecurity.addFilterBefore(tokenFilter, UsernamePasswordAuthenticationFilter.class);
        // 禁用緩存
        httpSecurity.headers().cacheControl();
    }
}

至此,Token 的安全配置結束。

2.3)API 接口和接口服務層

新建 service 包,新建接口服務層 SysUserService:

@Service
public class SysUserService {
    @Resource
    private AuthenticationManager authenticationManager;

    @Resource
    private TokenUtils tokenUtils;

    @Resource
    private SysUserMapper sysUserMapper;

    /** 用戶登錄
     * @param sysUser 用戶登錄信息
     * @return 用戶登錄成功返回的Token
     */
    public MyResponse login(final SysUser sysUser) {
        try {
            // 驗證用戶名和密碼是否對的
            System.out.println(sysUser.getUsername());
            authenticationManager.authenticate(
                    new UsernamePasswordAuthenticationToken(sysUser.getUsername(),
                            sysUser.getPassword()));
        } catch (BadCredentialsException e) {
            return new MyResponse("ERROR", "用戶名或者密碼不正確");
        }
        // 生成Token與查詢用戶權限
        SysUser sysUserData = sysUserMapper.selectByUsername(sysUser.getUsername());
        return new MyResponse("SUCCESS",
                tokenUtils.createToken(sysUserData));
    }

    /** 用戶註冊
     * @param sysUser 用戶註冊信息
     * @return 用戶註冊結果
     */
    public MyResponse save(SysUser sysUser) throws DataAccessException {
        try {
            // 密碼加密存儲
            BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
            String password = bCryptPasswordEncoder.encode(sysUser.getPassword());
            sysUser.setPassword(password);
            sysUserMapper.insert(sysUser);
        } catch (DataAccessException e) {
            return new MyResponse("ERROR", "已經存在該用戶名或者用戶暱稱,或者用戶權限出錯");
        }
        return new MyResponse("SUCCESS", "用戶新增成功");
    }
}

SysUserController :RESTful API 接口, @PreAuthorize(“hasRole(‘ADMIN’)”) 指定具有 ADMIN 權限的用戶纔可以訪問

@RestController
@RequestMapping(value = "/api/user")
public class SysUserController {
    @Resource
    private SysUserService sysUserService;

    /** 用戶登錄接口
     * @param sysUser 用戶登錄的用戶名和密碼
     * @return 用戶Token和角色
     * @throws AuthenticationException 身份驗證錯誤拋出異常
     */
    @PostMapping(value = "/login")
    public MyResponse login(@RequestBody final SysUser sysUser) throws AuthenticationException {
        return sysUserService.login(sysUser);
    }

    /** 用戶註冊接口
     * @param sysUser 用戶註冊信息
     * @return 用戶註冊結果
     */
    @PostMapping(value = "/register")
    public MyResponse register(@RequestBody @Valid final SysUser sysUser) {
        return sysUserService.save(sysUser);
    }

    /** 這是登錄用戶纔可以看到的內容 */
    @PostMapping(value = "/message")
    public String message() {
        return "這個消息只有登錄用戶纔可以看到";
    }

    /** 這是管理員用戶纔可以看到 */
    @PostMapping(value = "/admin")
    @PreAuthorize("hasRole('ADMIN')")
    public String admin() {
        return "這個消息只有管理員用戶纔可以看到";
    }
}

2.4)運行

運行項目分別註冊、登錄和訪問授權用戶接口。

註冊: 分別註冊 ADMIN、USER 權限的用戶

登錄:

使用 Token 授權:/api/user/admin 和 /api/user/message

附錄,,Maven 項目配置

<dependencies>
    <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>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.1.2</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-api</artifactId>
        <version>0.11.1</version>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-impl</artifactId>
        <version>0.11.1</version>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-jackson</artifactId>        <!-- or jjwt-gson if Gson is preferred -->
        <version>0.11.1</version>
        <scope>runtime</scope>
    </dependency>
    <!-- Uncomment this next dependency if you are using JDK 10 or earlier and you also want to use 
RSASSA-PSS (PS256, PS384, PS512) algorithms.  JDK 11 or later does not require it for those algorithms:
<dependency>
<groupId>org.bouncycastle</groupId>
    <artifactId>bcprov-jdk15on</artifactId>
    <version>1.60</version>
<scope>runtime</scope>
</dependency> -->
</dependencies>

本小節主要實戰了 Spring Boot 整合 Spring Security,JJWT,實現 Token 認證和授權的 RESTful API 接口,實現用戶的註冊、登錄和角色控制的功能。下一小節,將實戰 Spring Boot 整合另一個常用的安全框架 Apache Shiro 實現 Token 認證和授權的 RESTful API 接口。接下來的序列文章安排

  • Apache Shiro 實現 Token 認證和授權的 RESTful API
  • Apache Shiro 實現微信掃碼登錄
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章