SpringBoot+Security+JWT基礎

title: SpringBoot+Security+JWT基礎
date: 2019-07-04
author: maxzhao
tags:
  - JAVA
  - SpringBoot
  - Security
  - JWT
categories:
  - SpringBoot
  - Security

First

  • 我第一次使用譬重,所以代碼中的注釋和說明比較多

優(yōu)點

  • 由于服務器不保存 session 狀態(tài),因此無法在使用過程中廢止某個 token磅甩,或者更改 token 的權(quán)限。也就是說彩倚,一旦 JWT 簽發(fā)了筹我,在到期之前就會始終有效,除非服務器部署額外的邏輯帆离。

缺點

Security 基本原理

下面每個類或接口的作用,之后都會有代碼.


了解 Token結(jié)構(gòu)

Token是一個很長的字符串蔬蕊,中間用點(.)分隔成三個部分。

JWT 的三個部分依次如下岸夯。

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

Header(頭部)

{
  "alg": "HS256",
  "typ": "JWT"
}

alg屬性表示簽名的算法(algorithm)破镰,默認是 HMAC SHA256(寫成 HS256)集惋;typ屬性表示這個令牌(token)的類型(type)养渴,JWT 令牌統(tǒng)一寫為JWT翘紊。

Payload(負載)

Payload 部分也是一個 JSON 對象踪宠,用來存放實際需要傳遞的數(shù)據(jù)。JWT 規(guī)定了7個官方字段,供選用涩馆。

  • iss (issuer):簽發(fā)人
  • exp (expiration time):過期時間
  • sub (subject):主題
  • aud (audience):受眾
  • nbf (Not Before):生效時間
  • iat (Issued At):簽發(fā)時間
  • jti (JWT ID):編號

除了官方字段涯雅,你還可以在這個部分定義私有字段,下面就是一個例子怒允。

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

注意,JWT 默認是不加密的钾唬,任何人都可以讀到,所以不要把秘密信息放在這個部分懦尝。

Signature(簽名)

Signature 部分是對前兩部分的簽名知纷,防止數(shù)據(jù)篡改。

首先陵霉,需要指定一個密鑰(secret)琅轧。這個密鑰只有服務器才知道,不能泄露給用戶踊挠。然后乍桂,使用 Header 里面指定的簽名算法(默認是 HMAC SHA256)冲杀,按照下面的公式產(chǎn)生簽名。

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

算出簽名以后睹酌,把 Header权谁、Payload、Signature 三個部分拼成一個字符串憋沿,每個部分之間用"點"(.)分隔旺芽,就可以返回給用戶。

思路

  1. 構(gòu)建
  2. 導入 security 辐啄、 jwt 依賴
  3. 用戶的驗證(service 采章、 dao 、model)
  4. 實現(xiàn)UserDetailsService 壶辜、UserDetails接口
  5. 可選:實現(xiàn)PasswordEncoder 接口(密碼加密)
  6. 驗證用戶登錄信息悯舟、用戶權(quán)限的攔截器
  7. security 配置
  8. 登錄認證 API

構(gòu)建

1. 構(gòu)建

創(chuàng)建個項目

2.導入 security 、 jwt 依賴

<spring-security-jwt.version>1.0.9.RELEASE</spring-security-jwt.version>
<jjwt.version>0.9.1</jjwt.version>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>${jjwt.version}</version>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-jwt</artifactId>
    <version>${spring-security-jwt.version}</version>
    <scope>compile</scope>
</dependency>
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>
<dependency>
   <groupId>com.alibaba</groupId>
   <artifactId>druid-spring-boot-starter</artifactId>
   <version>1.1.16</version>
</dependency>
<!--使用啦Lombok插件砸民,需要自己添加 其它需要自己添加了-->

3.用戶的驗證(service 抵怎、 dao 、model)

model

/**
 * 用戶表
 *
 *
 * @author maxzhao
 * @date 2019-6-6 13:53:17
 */
@Accessors(chain = true)
@Data
@Entity
@Table(name = "app_user", schema = "", catalog = "")
@ApiModel(value = "用戶表", description = "用戶表")
public class AppUser implements Serializable {
    private static final long serialVersionUID = -1L;

    @Id
    @Column(name = "ID",unique = true)
    private Long id;

    @Basic
    @Column(name = "LIVE_ADDRESS")
    private String liveAddress;

    @Basic
    @Column(name = "LOGIN_NAME")
    private String loginName;
    @Basic
    @Column(name = "PASSWORD")
    private String password;
/* 省略 其它*/
}

dao

/**
 * 用戶表
 * Repository
 *
 * @author maxzhao
 * @date 2019-5-21 11:17:39
 */
@Repository(value = "appUserRepository")
public interface AppUserRepository extends JpaRepository<AppUser, Long>, JpaSpecificationExecutor<AppUser> {
    /**
     * 根據(jù)登錄名 查詢當前用戶
     *
     * @param loginName 登錄名
     * @return
     * @author maxzhao
     * @date 2019-05-22
     */
    List<AppUser> findByLoginNameEquals(String loginName);

}

service


/**
 * 用戶表
 * Service
 *
 * @author maxzhao
 * @date 2019-5-21 11:17:39
 */
public interface AppUserService {
    /**
     * 保存
     *
     * @param appUser
     * @return
     * @author maxzhao
     * @date 2019-06-19
     */
    AppUser saveOne(AppUser appUser);

    /**
     * 根據(jù)登錄名查詢 當前登錄用戶
     *
     * @param loginName
     * @return
     */
    AppUser findByLoginName(String loginName);
}
/**
 * 用戶表
 * ServiceImpl
 *
 * @author maxzhao
 * @date 2019-5-21 11:17:39
 */
@Service(value = "appUserService")
public class AppUserServiceImpl implements AppUserService {

    @Resource(name = "appUserRepository")
    private AppUserRepository appUserRepository;

    @Override
    public AppUser saveOne(AppUser appUser) {
        return appUserRepository.save(appUser);
    }

    @Override
    public AppUser findByLoginName(String loginName) {
        List<AppUser> appUserList = appUserRepository.findByLoginNameEquals(loginName);
        return appUserList.size() > 0 ? appUserList.get(0) : null;
    }
}

Jwt 工具類

/**
 * <p>jjwt封裝一下方便調(diào)用</p>
 * <p>JwtTokenUtil</p>
 *
 * @author maxzhao
 * @date 2019-07-04 13:30
 */
public class JwtTokenUtil {

    /**
     * 密鑰
     */
    private static final String SECRET = "jwt_secret_gtboot";
    private static final String ISS = "gtboot";

    /**
     * 過期時間是 1800 秒
     */
    private static final long EXPIRATION = 1800L;

    public static String createToken(String issuer, String subject, long expiration) {
        return createToken(issuer, subject, expiration, null);
    }

    /**
     * 創(chuàng)建 token
     *
     * @param issuer     簽發(fā)人
     * @param subject    主體,即用戶信息的JSON
     * @param expiration 有效時間(秒)
     * @param claims     自定義參數(shù)
     * @return
     * @description todo https://www.cnblogs.com/wangshouchang/p/9551748.html
     */
    public static String createToken(String issuer, String subject, long expiration, Claims claims) {
        return Jwts.builder()
                // JWT_ID:是JWT的唯一標識岭参,根據(jù)業(yè)務需要反惕,這個可以設置為一個不重復的值,主要用來作為一次性token,從而回避重放攻擊冗荸。
//                .setId(id)
                // 簽名算法以及密匙
                .signWith(SignatureAlgorithm.HS512, SECRET)
                // 自定義屬性
                .setClaims(null)
                // 主題:代表這個JWT的主體承璃,即它的所有人,這個是一個json格式的字符串蚌本,可以存放什么userid,roldid之類的隘梨,作為什么用戶的唯一標志程癌。
                .setSubject(subject)
                // 受眾
//                .setAudience(loginName)
                // 簽發(fā)人
                .setIssuer(Optional.ofNullable(issuer).orElse(ISS))
                // 簽發(fā)時間
                .setIssuedAt(new Date())
                // 過期時間
                .setExpiration(new Date(System.currentTimeMillis() + (expiration > 0 ? expiration : EXPIRATION) * 1000))
                .compact();
    }

    /**
     * 從 token 中獲取主題信息
     *
     * @param token
     * @return
     */
    public static String getProperties(String token) {
        return getTokenBody(token).getSubject();
    }

    /**
     * 校驗是否過期
     *
     * @param token
     * @return
     */
    public static boolean isExpiration(String token) {
        return getTokenBody(token).getExpiration().before(new Date());
    }

    /**
     * 獲得 token 的 body
     *
     * @param token
     * @return
     */
    private static Claims getTokenBody(String token) {
        return Jwts.parser()
                .setSigningKey(SECRET)
                .parseClaimsJws(token)
                .getBody();
    }
}

4.實現(xiàn)UserDetailsServiceUserDetails接口

UserDetailsService

可以把角色相關禁用掉,然后修改參數(shù).

我也會把角色相關操作,以及表的 sql 放到最后.

/**
 * 加載特定于用戶的數(shù)據(jù)的核心接口轴猎。
 * 它作為用戶DAO在整個框架中使用嵌莉,是DaoAuthenticationProvider使用的策略。
 * 該接口只需要一個只讀方法捻脖,這簡化了對新數(shù)據(jù)訪問策略的支持锐峭。
 *
 * @author maxzhao
 */
@Service("userDetailsService")
public class UserDetailsServiceImpl implements UserDetailsService {
    private static final Logger logger = LoggerFactory.getLogger(UserDetailsServiceImpl.class);
    /**
     * 用戶操作服務
     */
    @Resource(name = "appUserService")
    private AppUserService appUserService;

    /**
     * 用戶角色服務
     */
    @Resource(name = "appRoleService")
    private AppRoleService appRoleService;

    /**
     * 根據(jù)用戶登錄名定位用戶。
     *
     * @param loginName
     * @return
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String loginName) throws UsernameNotFoundException {

        UserDetails userDetails = null;
        try {
            AppUser appUser = appUserService.findByLoginName(loginName);
            if (appUser != null) {
                // 查詢當前用戶的權(quán)限
                List<AppRole> appRoleList = appRoleService.findByUserId(appUser.getId());
                Collection<GrantedAuthority> authorities = new ArrayList<>();
                for (AppRole appRole : appRoleList) {
                    SimpleGrantedAuthority grant = new SimpleGrantedAuthority(appRole.getConstName());
                    authorities.add(grant);
                }
                //封裝自定義UserDetails類
                userDetails = new UserDetailsImpl(appUser, authorities);
            } else {
                throw new UsernameNotFoundException("該用戶不存在可婶!");
            }
        } catch (Exception e) {
            logger.error(e.getMessage());
        }
        return userDetails;
    }

}

UserDetails

/**
 * 自定義用戶身份信息
 * 提供核心用戶信息沿癞。
 * 出于安全目的,Spring Security不直接使用實現(xiàn)矛渴。它們只是存儲用戶信息椎扬,這些信息稍后封裝到身份驗證對象中。這允許將非安全相關的用戶信息(如電子郵件地址、電話號碼等)存儲在一個方便的位置蚕涤。
 * 具體實現(xiàn)必須特別注意筐赔,以確保每個方法的非空契約都得到了執(zhí)行。有關參考實現(xiàn)(您可能希望在代碼中對其進行擴展或使用)揖铜,請參見User茴丰。
 *
 * @author maxzhao
 * @date 2019-05-22
 */
public class UserDetailsImpl implements UserDetails {
    private static final long serialVersionUID = 1L;
    /**
     * 用戶信息
     */
    private AppUser appUser;
    /**
     * 用戶角色
     */
    private Collection<? extends GrantedAuthority> authorities;

    public UserDetailsImpl(AppUser appUser, Collection<? extends GrantedAuthority> authorities) {
        super();
        this.appUser = appUser;
        this.authorities = authorities;
    }

    /**
     * 返回用戶所有角色的封裝,一個Role對應一個GrantedAuthority
     *
     * @return 返回授予用戶的權(quán)限天吓。
     */
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
    /*    Collection<GrantedAuthority> authorities = new ArrayList<>();
        String username = this.getUsername();
        if (username != null) {
            SimpleGrantedAuthority authority = new SimpleGrantedAuthority(username);
            authorities.add(authority);
        }*/
        return authorities;
    }
    /**
     * 返回用于驗證用戶身份的密碼较沪。
     *
     * @return Returns the password used to authenticate the user.
     */
    @Override
    public String getPassword() {
        return appUser.getPassword();
    }
    /**
     * @return
     */
    @Override
    public String getUsername() {
        return appUser.getLoginName();
    }
    /**
     * 判斷賬號是否已經(jīng)過期,默認沒有過期
     *
     * @return true 沒有過期
     */
    @Override
    public boolean isAccountNonExpired() {
        return appUser.getExpiration() == null || appUser.getExpiration().before(new Date());
    }
    /**
     * 判斷賬號是否被鎖定失仁,默認沒有鎖定
     *
     * @return true 沒有鎖定  false 鎖定
     */
    @Override
    public boolean isAccountNonLocked() {
        return appUser.getLockStatus() == null || appUser.getLockStatus() == 0;
    }
    /**
     * todo 判斷信用憑證是否過期尸曼,默認沒有過期
     *
     * @return
     */
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
    /**
     * 判斷賬號是否可用,默認可用
     *
     * @return
     */
    @Override
    public boolean isEnabled() {
        return appUser.getDelStatus() == 0;
    }
}

5.可選:實現(xiàn)PasswordEncoder 接口(密碼加密)

/**
 * PasswordEncoderImpl
 *
 * @author maxzhao
 * @date 2019-05-23 15:55
 */
@Service("passwordEncoder")
public class PasswordEncoderImpl implements PasswordEncoder {
    private final int strength;
    private final SecureRandom random;
    private Pattern BCRYPT_PATTERN;
    private Logger logger;

    /**
     * 構(gòu)造函數(shù)用于設置不同的加密過程
     */
    public PasswordEncoderImpl() {
        this(-1);
    }

    public PasswordEncoderImpl(int strength) {
        this(strength, null);
    }

    public PasswordEncoderImpl(int strength, SecureRandom random) {
        this.BCRYPT_PATTERN = Pattern.compile("\\A\\$2a?\\$\\d\\d\\$[./0-9A-Za-z]{53}");
        this.logger = LoggerFactory.getLogger(this.getClass());
        if (strength == -1 || strength >= 4 && strength <= 31) {
            this.strength = strength;
            this.random = random;
        } else {
            throw new IllegalArgumentException("Bad strength");
        }
    }

    /**
     * 對原始密碼進行編碼萄焦。通常控轿,一個好的編碼算法應用SHA-1或更大的哈希值和一個8字節(jié)或更大的隨機生成的salt。
     * Encode the raw password. Generally, a good encoding algorithm applies a SHA-1 or greater hash combined with an 8-byte or greater randomly generated salt.
     *
     * @param rawPassword
     * @return
     */
    @Override
    public String encode(CharSequence rawPassword) {
        String salt;
        if (this.strength > 0) {
            if (this.random != null) {
                salt = BCrypt.gensalt(this.strength, this.random);
            } else {
                salt = BCrypt.gensalt(this.strength);
            }
        } else {
            salt = BCrypt.gensalt();
        }

        return BCrypt.hashpw(rawPassword.toString(), salt);
    }

    /**
     * 驗證從存儲中獲得的已編碼密碼在經(jīng)過編碼后是否與提交的原始密碼匹配拂封。
     * 如果密碼匹配茬射,返回true;如果密碼不匹配,返回false冒签。存儲的密碼本身永遠不會被解碼在抛。
     *
     * @param rawPassword     the raw password to encode and match
     * @param encodedPassword the encoded password from storage to compare with
     * @return
     */
    @Override
    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        if (encodedPassword != null && encodedPassword.length() != 0) {
            if (!this.BCRYPT_PATTERN.matcher(encodedPassword).matches()) {
                this.logger.warn("Encoded password does not look like BCrypt");
                return false;
            } else {
                return BCrypt.checkpw(rawPassword.toString(), encodedPassword);
            }
        } else {
            this.logger.warn("Empty encoded password");
            return false;
        }
    }

    /**
     * 如果為了更好的安全性,應該再次對已編碼的密碼進行編碼萧恕,則返回true刚梭,否則為false。
     *
     * @param encodedPassword the encoded password to check
     * @return Returns true if the encoded password should be encoded again for better security, else false. The default implementation always returns false.
     */
    @Override
    public boolean upgradeEncoding(String encodedPassword) {
        return false;
    }
}

6.驗證用戶登錄信息票唆、用戶權(quán)限的攔截器

  • JwtAuthenticationFilter用戶賬號的驗證
  • JwtAuthorizationFilter用戶權(quán)限的驗證

JwtAuthenticationFilter繼承于UsernamePasswordAuthenticationFilter
該攔截器用于獲取用戶登錄的信息朴读,只需創(chuàng)建一個 token并調(diào)用 authenticationManager.authenticate()spring-security去進行驗證就可以了,不用自己查數(shù)據(jù)庫再對比密碼了走趋,這一步交給spring去操作衅金。

這個操作有點像是shirosubject.login(new UsernamePasswordToken()),驗證的事情交給框架簿煌。

7.security 配置

8.登錄認證 API

配置文件

spring:
  datasource:
    driverClassName: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/maxzhao_ittest?charset=utf8mb4&useSSL=false
    username: maxzhao
    password: maxzhao
  main:
    allow-bean-definition-overriding: true

  jpa:
    database: MYSQL
    database-plinatform: org.hibernate.dialect.MySQL5InnoDBDialect
    show-sql: true
    generate-ddl: true
    open-in-view: false

    hibernate:
      ddl-auto: update
    #       naming-strategy: org.hibernate.cfg.ImprovedNamingStrategy
    properties:
      #不加此配置氮唯,獲取不到當前currentsession
      hibernate:
        current_session_context_class: org.springframework.orm.hibernate5.SpringSessionContext
        dialect: org.hibernate.dialect.MySQL5Dialect
# 多數(shù)據(jù)源配置
gt:
  maxzhao:
    boot:
    #主動開啟多數(shù)據(jù)源
      multiDatasourceOpen: true
      datasource[0]:
        dbName: second
        driverClassName: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://127.0.0.1:3306/pos?charset=utf8mb4&useSSL=false
        username: maxzhao
        password: maxzhao
      datasource[1]:
        dbName: third
        driverClassName: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://127.0.0.1:3306/biz?charset=utf8mb4&useSSL=false
        username: maxzhao
        password: maxzhao

本文地址:
SpringBoot+Security+JWT基礎

gitee

推薦
SpringBoot+Security+JWT基礎
SpringBoot+Security+JWT進階:一、自定義認證
SpringBoot+Security+JWT進階:二姨伟、自定義認證實踐

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末惩琉,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子授滓,更是在濱河造成了極大的恐慌琳水,老刑警劉巖肆糕,帶你破解...
    沈念sama閱讀 211,817評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異在孝,居然都是意外死亡诚啃,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,329評論 3 385
  • 文/潘曉璐 我一進店門私沮,熙熙樓的掌柜王于貴愁眉苦臉地迎上來始赎,“玉大人,你說我怎么就攤上這事仔燕≡於猓” “怎么了?”我有些...
    開封第一講書人閱讀 157,354評論 0 348
  • 文/不壞的土叔 我叫張陵晰搀,是天一觀的道長五辽。 經(jīng)常有香客問我,道長外恕,這世上最難降的妖魔是什么杆逗? 我笑而不...
    開封第一講書人閱讀 56,498評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮鳞疲,結(jié)果婚禮上罪郊,老公的妹妹穿的比我還像新娘。我一直安慰自己尚洽,他們只是感情好悔橄,可當我...
    茶點故事閱讀 65,600評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著腺毫,像睡著了一般癣疟。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上拴曲,一...
    開封第一講書人閱讀 49,829評論 1 290
  • 那天争舞,我揣著相機與錄音,去河邊找鬼澈灼。 笑死,一個胖子當著我的面吹牛店溢,可吹牛的內(nèi)容都是我干的叁熔。 我是一名探鬼主播,決...
    沈念sama閱讀 38,979評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼床牧,長吁一口氣:“原來是場噩夢啊……” “哼荣回!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起戈咳,我...
    開封第一講書人閱讀 37,722評論 0 266
  • 序言:老撾萬榮一對情侶失蹤心软,失蹤者是張志新(化名)和其女友劉穎壕吹,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體删铃,經(jīng)...
    沈念sama閱讀 44,189評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡耳贬,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,519評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了猎唁。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片咒劲。...
    茶點故事閱讀 38,654評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖诫隅,靈堂內(nèi)的尸體忽然破棺而出腐魂,到底是詐尸還是另有隱情,我是刑警寧澤逐纬,帶...
    沈念sama閱讀 34,329評論 4 330
  • 正文 年R本政府宣布蛔屹,位于F島的核電站,受9級特大地震影響豁生,放射性物質(zhì)發(fā)生泄漏兔毒。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,940評論 3 313
  • 文/蒙蒙 一沛硅、第九天 我趴在偏房一處隱蔽的房頂上張望眼刃。 院中可真熱鬧,春花似錦摇肌、人聲如沸擂红。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,762評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽昵骤。三九已至,卻和暖如春肯适,著一層夾襖步出監(jiān)牢的瞬間变秦,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,993評論 1 266
  • 我被黑心中介騙來泰國打工框舔, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留蹦玫,地道東北人。 一個月前我還...
    沈念sama閱讀 46,382評論 2 360
  • 正文 我出身青樓刘绣,卻偏偏與公主長得像樱溉,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子纬凤,可洞房花燭夜當晚...
    茶點故事閱讀 43,543評論 2 349