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 三個部分拼成一個字符串憋沿,每個部分之間用"點"(.
)分隔旺芽,就可以返回給用戶。
思路
- 構(gòu)建
- 導入 security 辐啄、 jwt 依賴
- 用戶的驗證(service 采章、 dao 、model)
- 實現(xiàn)
UserDetailsService
壶辜、UserDetails
接口 - 可選:實現(xiàn)
PasswordEncoder
接口(密碼加密) - 驗證用戶登錄信息悯舟、用戶權(quán)限的攔截器
- security 配置
- 登錄認證 API
構(gòu)建
1. 構(gòu)建
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)UserDetailsService
、UserDetails
接口
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去操作衅金。
這個操作有點像是shiro
的 subject.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基礎推薦
SpringBoot+Security+JWT基礎
SpringBoot+Security+JWT進階:一、自定義認證
SpringBoot+Security+JWT進階:二姨伟、自定義認證實踐