Spring Boot(四)之使用JWT和Spring Security保護(hù)REST API
通常情況下,把API直接暴露出去是風(fēng)險(xiǎn)很大的,不說(shuō)別的,直接被機(jī)器攻擊就喝一壺的。那么一般來(lái)說(shuō),對(duì)API要?jiǎng)澐殖鲆欢ǖ臋?quán)限級(jí)別,然后做一個(gè)用戶的鑒權(quán),依據(jù)鑒權(quán)結(jié)果給予用戶開(kāi)放對(duì)應(yīng)的API。目前,比較主流的方案有幾種:
- 用戶名和密碼鑒權(quán),使用Session保存用戶鑒權(quán)結(jié)果。
- 使用OAuth進(jìn)行鑒權(quán)(其實(shí)OAuth也是一種基于Token的鑒權(quán),只是沒(méi)有規(guī)定Token的生成方式)
- 自行采用Token進(jìn)行鑒權(quán)
第一種就不介紹了,由于依賴Session來(lái)維護(hù)狀態(tài),也不太適合移動(dòng)時(shí)代,新的項(xiàng)目就不要采用了。第二種OAuth的方案和JWT都是基于Token的,但OAuth其實(shí)對(duì)于不做開(kāi)放平臺(tái)的公司有些過(guò)于復(fù)雜。我們主要介紹第三種:JWT。
什么是JWT?
JWT是 Json Web Token 的縮寫(xiě)。它是基于 RFC 7519 標(biāo)準(zhǔn)定義的一種可以安全傳輸?shù)?小巧 和 自包含 的JSON對(duì)象。由于數(shù)據(jù)是使用數(shù)字簽名的,所以是可信任的和安全的。JWT可以使用HMAC算法對(duì)secret進(jìn)行加密或者使用RSA的公鑰私鑰對(duì)來(lái)進(jìn)行簽名。
JWT的工作流程
下面是一個(gè)JWT的工作流程圖。模擬一下實(shí)際的流程是這樣的(假設(shè)受保護(hù)的API在/protected中)
- 用戶導(dǎo)航到登錄頁(yè),輸入用戶名、密碼,進(jìn)行登錄
- 服務(wù)器驗(yàn)證登錄鑒權(quán),如果改用戶合法,根據(jù)用戶的信息和服務(wù)器的規(guī)則生成JWT Token
- 服務(wù)器將該token以json形式返回(不一定要json形式,這里說(shuō)的是一種常見(jiàn)的做法)
- 用戶得到token,存在localStorage、cookie或其它數(shù)據(jù)存儲(chǔ)形式中。
- 以后用戶請(qǐng)求/protected中的API時(shí),在請(qǐng)求的header中加入 Authorization: Bearer xxxx(token)。此處注意token之前有一個(gè)7字符長(zhǎng)度的 Bearer
- 服務(wù)器端對(duì)此token進(jìn)行檢驗(yàn),如果合法就解析其中內(nèi)容,根據(jù)其擁有的權(quán)限和自己的業(yè)務(wù)邏輯給出對(duì)應(yīng)的響應(yīng)結(jié)果。
- 用戶取得結(jié)果

JWT工作流程圖
為了更好的理解這個(gè)token是什么,我們先來(lái)看一個(gè)token生成后的樣子,下面那坨亂糟糟的就是了。
eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ3YW5nIiwiY3JlYXRlZCI6MTQ4OTA3OTk4MTM5MywiZXhwIjoxNDg5Njg0NzgxfQ.RC-BYCe_UZ2URtWddUpWXIp4NMsoeq2O6UF-8tVplqXY1-CI9u1-a-9DAAJGfNWkHE81mpnR3gXzfrBAB3WUAg
但仔細(xì)看到的話還是可以看到這個(gè)token分成了三部分,每部分用 . 分隔,每段都是用 Base64 編碼的。如果我們用一個(gè)Base64的解碼器的話 ( https://www.base64decode.org/ ),可以看到第一部分 eyJhbGciOiJIUzUxMiJ9 被解析成了:
{
"alg":"HS512"
}
這是告訴我們HMAC采用HS512算法對(duì)JWT進(jìn)行的簽名。
第二部分 eyJzdWIiOiJ3YW5nIiwiY3JlYXRlZCI6MTQ4OTA3OTk4MTM5MywiZXhwIjoxNDg5Njg0NzgxfQ 被解碼之后是
{
"sub":"wang",
"created":1489079981393,
"exp":1489684781
}
這段告訴我們這個(gè)Token中含有的數(shù)據(jù)聲明(Claim),這個(gè)例子里面有三個(gè)聲明:sub, created 和 exp。在我們這個(gè)例子中,分別代表著用戶名、創(chuàng)建時(shí)間和過(guò)期時(shí)間,當(dāng)然你可以把任意數(shù)據(jù)聲明在這里。
看到這里,你可能會(huì)想這是個(gè)什么鬼token,所有信息都透明啊,安全怎么保障?別急,我們看看token的第三段 RC-BYCe_UZ2URtWddUpWXIp4NMsoeq2O6UF-8tVplqXY1-CI9u1-a-9DAAJGfNWkHE81mpnR3gXzfrBAB3WUAg。同樣使用Base64解碼之后,咦,這是什么東東
D X DmYTeȧLUZcPZ0$gZAY_7wY@
最后一段其實(shí)是簽名,這個(gè)簽名必須知道秘鑰才能計(jì)算。這個(gè)也是JWT的安全保障。這里提一點(diǎn)注意事項(xiàng),由于數(shù)據(jù)聲明(Claim)是公開(kāi)的,千萬(wàn)不要把密碼等敏感字段放進(jìn)去,否則就等于是公開(kāi)給別人了。
也就是說(shuō)JWT是由三段組成的,按官方的叫法分別是header(頭)、payload(負(fù)載)和signature(簽名):
header.payload.signature
頭中的數(shù)據(jù)通常包含兩部分:一個(gè)是我們剛剛看到的 alg,這個(gè)詞是 algorithm 的縮寫(xiě),就是指明算法。另一個(gè)可以添加的字段是token的類型(按RFC 7519實(shí)現(xiàn)的token機(jī)制不只JWT一種),但如果我們采用的是JWT的話,指定這個(gè)就多余了。
{
"alg": "HS512",
"typ": "JWT"
}
payload中可以放置三類數(shù)據(jù):系統(tǒng)保留的、公共的和私有的:
- 系統(tǒng)保留的聲明(Reserved claims):這類聲明不是必須的,但是是建議使用的,包括:iss (簽發(fā)者), exp (過(guò)期時(shí)間),
- sub (主題), aud (目標(biāo)受眾)等。這里我們發(fā)現(xiàn)都用的縮寫(xiě)的三個(gè)字符,這是由于JWT的目標(biāo)就是盡可能小巧。
- 公共聲明:這類聲明需要在 IANA JSON Web Token Registry 中定義或者提供一個(gè)URI,因?yàn)橐苊庵孛葲_突。
- 私有聲明:這個(gè)就是你根據(jù)業(yè)務(wù)需要自己定義的數(shù)據(jù)了。
簽名的過(guò)程是這樣的:采用header中聲明的算法,接受三個(gè)參數(shù):base64編碼的header、base64編碼的payload和秘鑰(secret)進(jìn)行運(yùn)算。簽名這一部分如果你愿意的話,可以采用RSASHA256的方式進(jìn)行公鑰、私鑰對(duì)的方式進(jìn)行,如果安全性要求的高的話。
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
JWT的生成和解析
為了簡(jiǎn)化我們的工作,這里引入一個(gè)比較成熟的JWT類庫(kù),叫 jjwt ( https://github.com/jwtk/jjwt )。這個(gè)類庫(kù)可以用于Java和Android的JWT token的生成和驗(yàn)證。
JWT的生成可以使用下面這樣的代碼完成:
String generateToken(Map<String, Object> claims) {
return Jwts.builder()
.setClaims(claims)
.setExpiration(generateExpirationDate())
.signWith(SignatureAlgorithm.HS512, secret) //采用什么算法是可以自己選擇的,不一定非要采用HS512
.compact();
}
數(shù)據(jù)聲明(Claim)其實(shí)就是一個(gè)Map,比如我們想放入用戶名,可以簡(jiǎn)單的創(chuàng)建一個(gè)Map然后put進(jìn)去就可以了。
Map<String, Object> claims = new HashMap<>(); claims.put(CLAIM_KEY_USERNAME, username());
解析也很簡(jiǎn)單,利用 jjwt 提供的parser傳入秘鑰,然后就可以解析token了。
Claims getClaimsFromToken(String token) {
Claims claims;
try {
claims = Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
} catch (Exception e) {
claims = null;
}
return claims;
}
JWT本身沒(méi)啥難度,但安全整體是一個(gè)比較復(fù)雜的事情,JWT只不過(guò)提供了一種基于token的請(qǐng)求驗(yàn)證機(jī)制。但我們的用戶權(quán)限,對(duì)于API的權(quán)限劃分、資源的權(quán)限劃分,用戶的驗(yàn)證等等都不是JWT負(fù)責(zé)的。也就是說(shuō),請(qǐng)求驗(yàn)證后,你是否有權(quán)限看對(duì)應(yīng)的內(nèi)容是由你的用戶角色決定的。所以我們這里要利用Spring的一個(gè)子項(xiàng)目Spring Security來(lái)簡(jiǎn)化我們的工作。
Spring Security
Spring Security是一個(gè)基于Spring的通用安全框架,里面內(nèi)容太多了,本文的主要目的也不是展開(kāi)講這個(gè)框架,而是如何利用Spring Security和JWT一起來(lái)完成API保護(hù)。所以關(guān)于Spring Secruity的基礎(chǔ)內(nèi)容或展開(kāi)內(nèi)容,請(qǐng)自行去官網(wǎng)學(xué)習(xí)( http://projects.spring.io/spring-security/ )。
簡(jiǎn)單的背景知識(shí)
如果你的系統(tǒng)有用戶的概念的話,一般來(lái)說(shuō),你應(yīng)該有一個(gè)用戶表,最簡(jiǎn)單的用戶表,應(yīng)該有三列:Id,Username和Password,類似下表這種

而且不是所有用戶都是一種角色,比如網(wǎng)站管理員、供應(yīng)商、財(cái)務(wù)等等,這些角色和網(wǎng)站的直接用戶需要的權(quán)限可能是不一樣的。那么我們就需要一個(gè)角色表:

當(dāng)然我們還需要一個(gè)可以將用戶和角色關(guān)聯(lián)起來(lái)建立映射關(guān)系的表。

這是典型的一個(gè)關(guān)系型數(shù)據(jù)庫(kù)的用戶角色的設(shè)計(jì),由于我們要使用的MongoDB是一個(gè)文檔型數(shù)據(jù)庫(kù),所以讓我們重新審視一下這個(gè)結(jié)構(gòu)。
這個(gè)數(shù)據(jù)結(jié)構(gòu)的優(yōu)點(diǎn)在于它避免了數(shù)據(jù)的冗余,每個(gè)表負(fù)責(zé)自己的數(shù)據(jù),通過(guò)關(guān)聯(lián)表進(jìn)行關(guān)系的描述,同時(shí)也保證的數(shù)據(jù)的完整性:比如當(dāng)你修改角色名稱后,沒(méi)有臟數(shù)據(jù)的產(chǎn)生。
但是這種事情在用戶權(quán)限這個(gè)領(lǐng)域發(fā)生的頻率到底有多少呢?有多少人每天不停的改的角色名稱?當(dāng)然如果你的業(yè)務(wù)場(chǎng)景確實(shí)是需要保證數(shù)據(jù)完整性,你還是應(yīng)該使用關(guān)系型數(shù)據(jù)庫(kù)。但如果沒(méi)有高頻的對(duì)于角色表的改動(dòng),其實(shí)我們是不需要這樣的一個(gè)設(shè)計(jì)的。在MongoDB中我們可以將其簡(jiǎn)化為
{
_id: <id_generated>
username: 'user',
password: 'pass',
roles: ['USER', 'ADMIN']
}
基于以上考慮,我們重構(gòu)一下 User 類,
@Data
public class User {
@Id
private String id;
@Indexed(unique=true, direction= IndexDirection.DESCENDING, dropDups=true)
private String username;
private String password;
private String email;
private Date lastPasswordResetDate;
private List<String> roles;
}
當(dāng)然你可能發(fā)現(xiàn)這個(gè)類有點(diǎn)怪,只有一些field,這個(gè)簡(jiǎn)化的能力是一個(gè)叫l(wèi)ombok類庫(kù)提供的 ,這個(gè)很多開(kāi)發(fā)過(guò)Android的童鞋應(yīng)該熟悉,是用來(lái)簡(jiǎn)化POJO的創(chuàng)建的一個(gè)類庫(kù)。簡(jiǎn)單說(shuō)一下,采用 lombok 提供的 @Data 修飾符后可以簡(jiǎn)寫(xiě)成,原來(lái)的一坨getter和setter以及constructor等都不需要寫(xiě)了。類似的 Todo 可以改寫(xiě)成:
@Data
public class Todo {
@Id private String id;
private String desc;
private boolean completed;
private User user;
}
增加這個(gè)類庫(kù)只需在 build.gradle 中增加下面這行
dependencies {
// 省略其它依賴
compile("org.projectlombok:lombok:${lombokVersion}")
}
引入Spring Security
要在Spring Boot中引入Spring Security非常簡(jiǎn)單,修改 build.gradle,增加一個(gè)引用 org.springframework.boot:spring-boot-starter-security:
dependencies {
compile("org.springframework.boot:spring-boot-starter-data-rest")
compile("org.springframework.boot:spring-boot-starter-data-mongodb")
compile("org.springframework.boot:spring-boot-starter-security")
compile("io.jsonwebtoken:jjwt:${jjwtVersion}")
compile("org.projectlombok:lombok:${lombokVersion}")
testCompile("org.springframework.boot:spring-boot-starter-test")
}
你可能發(fā)現(xiàn)了,我們不只增加了對(duì)Spring Security的編譯依賴,還增加 jjwt 的依賴。
Spring Security需要我們實(shí)現(xiàn)幾個(gè)東西,第一個(gè)是UserDetails:這個(gè)接口中規(guī)定了用戶的幾個(gè)必須要有的方法,所以我們創(chuàng)建一個(gè)JwtUser類來(lái)實(shí)現(xiàn)這個(gè)接口。為什么不直接使用User類?因?yàn)檫@個(gè)UserDetails完全是為了安全服務(wù)的,它和我們的領(lǐng)域類可能有部分屬性重疊,但很多的接口其實(shí)是安全定制的,所以最好新建一個(gè)類:
public class JwtUser implements UserDetails {
private final String id;
private final String username;
private final String password;
private final String email;
private final Collection<? extends GrantedAuthority> authorities;
private final Date lastPasswordResetDate;
public JwtUser(
String id,
String username,
String password,
String email,
Collection<? extends GrantedAuthority> authorities,
Date lastPasswordResetDate) {
this.id = id;
this.username = username;
this.password = password;
this.email = email;
this.authorities = authorities;
this.lastPasswordResetDate = lastPasswordResetDate;
}
//返回分配給用戶的角色列表
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@JsonIgnore
public String getId() {
return id;
}
@JsonIgnore
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
// 賬戶是否未過(guò)期
@JsonIgnore
@Override
public boolean isAccountNonExpired() {
return true;
}
// 賬戶是否未鎖定
@JsonIgnore
@Override
public boolean isAccountNonLocked() {
return true;
}
// 密碼是否未過(guò)期
@JsonIgnore
@Override
public boolean isCredentialsNonExpired() {
return true;
}
// 賬戶是否激活
@JsonIgnore
@Override
public boolean isEnabled() {
return true;
}
// 這個(gè)是自定義的,返回上次密碼重置日期
@JsonIgnore
public Date getLastPasswordResetDate() {
return lastPasswordResetDate;
}
}
這個(gè)接口中規(guī)定的很多方法我們都簡(jiǎn)單粗暴的設(shè)成直接返回某個(gè)值了,這是為了簡(jiǎn)單起見(jiàn),你在實(shí)際開(kāi)發(fā)環(huán)境中還是要根據(jù)具體業(yè)務(wù)調(diào)整。當(dāng)然由于兩個(gè)類還是有一定關(guān)系的,為了寫(xiě)起來(lái)簡(jiǎn)單,我們寫(xiě)一個(gè)工廠類來(lái)由領(lǐng)域?qū)ο髣?chuàng)建 JwtUser,這個(gè)工廠就叫 JwtUserFactory 吧:
public final class JwtUserFactory {
private JwtUserFactory() {
}
public static JwtUser create(User user) {
return new JwtUser(
user.getId(),
user.getUsername(),
user.getPassword(),
user.getEmail(),
mapToGrantedAuthorities(user.getRoles()),
user.getLastPasswordResetDate()
);
}
private static List<GrantedAuthority> mapToGrantedAuthorities(List<String> authorities) {
return authorities.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}
}
第二個(gè)要實(shí)現(xiàn)的是 UserDetailsService,這個(gè)接口只定義了一個(gè)方法 loadUserByUsername,顧名思義,就是提供一種從用戶名可以查到用戶并返回的方法。注意,不一定是數(shù)據(jù)庫(kù)哦,文本文件、xml文件等等都可能成為數(shù)據(jù)源,這也是為什么Spring提供這樣一個(gè)接口的原因:保證你可以采用靈活的數(shù)據(jù)源。接下來(lái)我們建立一個(gè) JwtUserDetailsServiceImpl 來(lái)實(shí)現(xiàn)這個(gè)接口。
@Service
public class JwtUserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException(String.format("No user found with username '%s'.", username));
} else {
return JwtUserFactory.create(user);
}
}
}
為了讓Spring可以知道我們想怎樣控制安全性,我們還需要建立一個(gè)安全配置類 WebSecurityConfig:
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{
// Spring會(huì)自動(dòng)尋找同樣類型的具體類注入,這里就是JwtUserDetailsServiceImpl了
@Autowired
private UserDetailsService userDetailsService;
@Autowired
public void configureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
authenticationManagerBuilder
// 設(shè)置UserDetailsService
.userDetailsService(this.userDetailsService)
// 使用BCrypt進(jìn)行密碼的hash
.passwordEncoder(passwordEncoder());
}
// 裝載BCrypt密碼編碼器
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
// 由于使用的是JWT,我們這里不需要csrf
.csrf().disable()
// 基于token,所以不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests()
//.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
// 允許對(duì)于網(wǎng)站靜態(tài)資源的無(wú)授權(quán)訪問(wèn)
.antMatchers(
HttpMethod.GET,
"/",
"/*.html",
"/favicon.ico",
"/**/*.html",
"/**/*.css",
"/**/*.js"
).permitAll()
// 對(duì)于獲取token的rest api要允許匿名訪問(wèn)
.antMatchers("/auth/**").permitAll()
// 除上面外的所有請(qǐng)求全部需要鑒權(quán)認(rèn)證
.anyRequest().authenticated();
// 禁用緩存
httpSecurity.headers().cacheControl();
}
}
接下來(lái)我們要規(guī)定一下哪些資源需要什么樣的角色可以訪問(wèn)了,在 UserController 加一個(gè)修飾符 @PreAuthorize("hasRole('ADMIN')") 表示這個(gè)資源只能被擁有 ADMIN 角色的用戶訪問(wèn)。
/**
* 在 @PreAuthorize 中我們可以利用內(nèi)建的 SPEL 表達(dá)式:比如 'hasRole()' 來(lái)決定哪些用戶有權(quán)訪問(wèn)。
* 需注意的一點(diǎn)是 hasRole 表達(dá)式認(rèn)為每個(gè)角色名字前都有一個(gè)前綴 'ROLE_'。所以這里的 'ADMIN' 其實(shí)在
* 數(shù)據(jù)庫(kù)中存儲(chǔ)的是 'ROLE_ADMIN' 。這個(gè) @PreAuthorize 可以修飾Controller也可修飾Controller中的方法。
**/
@RestController
@RequestMapping("/users")
@PreAuthorize("hasRole('ADMIN')")
public class UserController {
@Autowired
private UserRepository repository;
@RequestMapping(method = RequestMethod.GET)
public List<User> getUsers() {
return repository.findAll();
}
// 略去其它部分
}
類似的我們給 TodoController 加上 @PreAuthorize("hasRole('USER')"),標(biāo)明這個(gè)資源只能被擁有 USER 角色的用戶訪問(wèn):
@RestController
@RequestMapping("/todos")
@PreAuthorize("hasRole('USER')")
public class TodoController {
// 略去
}
使用application.yml配置SpringBoot應(yīng)用
現(xiàn)在應(yīng)該Spring Security可以工作了,但為了可以更清晰的看到工作日志,我們希望配置一下,在和 src 同級(jí)建立一個(gè)config文件夾,在這個(gè)文件夾下面新建一個(gè) application.yml。
# Server configuration server: port: 8090 contextPath: # Spring configuration spring: jackson: serialization: INDENT_OUTPUT: true data.mongodb: host: localhost port: 27017 database: springboot # Logging configuration logging: level: org.springframework: data: DEBUG security: DEBUG
我們除了配置了logging的一些東東外,也順手設(shè)置了數(shù)據(jù)庫(kù)和http服務(wù)的一些配置項(xiàng),現(xiàn)在我們的服務(wù)器會(huì)在8090端口監(jiān)聽(tīng),而spring data和security的日志在debug模式下會(huì)輸出到console。
現(xiàn)在啟動(dòng)服務(wù)后,訪問(wèn) http://localhost:8090 你可以看到根目錄還是正常顯示的

根目錄還是正??梢栽L問(wèn)的
但我們?cè)囈幌?http://localhost:8090/users ,觀察一下console,我們會(huì)看到如下的輸出,告訴由于用戶未鑒權(quán),我們?cè)L問(wèn)被拒絕了。
2017-03-10 15:51:53.351 DEBUG 57599 --- [nio-8090-exec-4] o.s.s.w.a.ExceptionTranslationFilter : Access is denied (user is anonymous); redirecting to authentication entry point org.springframework.security.access.AccessDeniedException: Access is denied at org.springframework.security.access.vote.AffirmativeBased.decide(AffirmativeBased.java:84) ~[spring-security-core-4.2.1.RELEASE.jar:4.2.1.RELEASE]
集成JWT和Spring Security
到現(xiàn)在,我們還是讓JWT和Spring Security各自為戰(zhàn),并沒(méi)有集成起來(lái)。要想要JWT在Spring中工作,我們應(yīng)該新建一個(gè)filter,并把它配置在 WebSecurityConfig 中。
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Value("${jwt.header}")
private String tokenHeader;
@Value("${jwt.tokenHead}")
private String tokenHead;
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
String authHeader = request.getHeader(this.tokenHeader);
if (authHeader != null && authHeader.startsWith(tokenHead)) {
final String authToken = authHeader.substring(tokenHead.length()); // The part after "Bearer "
String username = jwtTokenUtil.getUsernameFromToken(authToken);
logger.info("checking authentication " + username);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
if (jwtTokenUtil.validateToken(authToken, userDetails)) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(
request));
logger.info("authenticated user " + username + ", setting security context");
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
}
chain.doFilter(request, response);
}
}
事實(shí)上如果我們足夠相信token中的數(shù)據(jù),也就是我們足夠相信簽名token的secret的機(jī)制足夠好,這種情況下,我們可以不用再查詢數(shù)據(jù)庫(kù),而直接采用token中的數(shù)據(jù)。本例中,我們還是通過(guò)Spring Security的 @UserDetailsService 進(jìn)行了數(shù)據(jù)查詢,但簡(jiǎn)單驗(yàn)證的話,你可以采用直接驗(yàn)證token是否合法來(lái)避免昂貴的數(shù)據(jù)查詢。
接下來(lái),我們會(huì)在 WebSecurityConfig 中注入這個(gè)filter,并且配置到 HttpSecurity 中:
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{
// 省略其它部分
@Bean
public JwtAuthenticationTokenFilter authenticationTokenFilterBean() throws Exception {
return new JwtAuthenticationTokenFilter();
}
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
// 省略之前寫(xiě)的規(guī)則部分,具體看前面的代碼
// 添加JWT filter
httpSecurity
.addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);
}
}
完成鑒權(quán)(登錄)、注冊(cè)和更新token的功能
到現(xiàn)在,我們整個(gè)API其實(shí)已經(jīng)在安全的保護(hù)下了,但我們遇到一個(gè)問(wèn)題:所有的API都安全了,但我們還沒(méi)有用戶啊,所以所有API都沒(méi)法訪問(wèn)。因此要提供一個(gè)注冊(cè)、登錄的API,這個(gè)API應(yīng)該是可以匿名訪問(wèn)的。給它規(guī)劃的路徑呢,我們前面其實(shí)在WebSecurityConfig中已經(jīng)給出了,就是 /auth。
首先需要一個(gè)AuthService,規(guī)定一下必選動(dòng)作:
public interface AuthService {
User register(User userToAdd);
String login(String username, String password);
String refresh(String oldToken);
}
然后,實(shí)現(xiàn)這些必選動(dòng)作,其實(shí)非常簡(jiǎn)單:
- 登錄時(shí)要生成token,完成Spring Security認(rèn)證,然后返回token給客戶端
- 注冊(cè)時(shí)將用戶密碼用BCrypt加密,寫(xiě)入用戶角色,由于是開(kāi)放注冊(cè),所以寫(xiě)入角色系統(tǒng)控制,將其寫(xiě)成 ROLE_USER
- 提供一個(gè)可以刷新token的接口 refresh 用于取得新的token
@Service
public class AuthServiceImpl implements AuthService {
private AuthenticationManager authenticationManager;
private UserDetailsService userDetailsService;
private JwtTokenUtil jwtTokenUtil;
private UserRepository userRepository;
@Value("${jwt.tokenHead}")
private String tokenHead;
@Autowired
public AuthServiceImpl(
AuthenticationManager authenticationManager,
UserDetailsService userDetailsService,
JwtTokenUtil jwtTokenUtil,
UserRepository userRepository) {
this.authenticationManager = authenticationManager;
this.userDetailsService = userDetailsService;
this.jwtTokenUtil = jwtTokenUtil;
this.userRepository = userRepository;
}
@Override
public User register(User userToAdd) {
final String username = userToAdd.getUsername();
if(userRepository.findByUsername(username)!=null) {
return null;
}
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
final String rawPassword = userToAdd.getPassword();
userToAdd.setPassword(encoder.encode(rawPassword));
userToAdd.setLastPasswordResetDate(new Date());
userToAdd.setRoles(asList("ROLE_USER"));
return userRepository.insert(userToAdd);
}
@Override
public String login(String username, String password) {
UsernamePasswordAuthenticationToken upToken = new UsernamePasswordAuthenticationToken(username, password);
final Authentication authentication = authenticationManager.authenticate(upToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
final UserDetails userDetails = userDetailsService.loadUserByUsername(username);
final String token = jwtTokenUtil.generateToken(userDetails);
return token;
}
@Override
public String refresh(String oldToken) {
final String token = oldToken.substring(tokenHead.length());
String username = jwtTokenUtil.getUsernameFromToken(token);
JwtUser user = (JwtUser) userDetailsService.loadUserByUsername(username);
if (jwtTokenUtil.canTokenBeRefreshed(token, user.getLastPasswordResetDate())){
return jwtTokenUtil.refreshToken(token);
}
return null;
}
}
然后建立AuthController就好,這個(gè)AuthController中我們?cè)谄渲惺褂昧吮磉_(dá)式綁定,比如 @Value("${jwt.header}")中的 jwt.header 其實(shí)是定義在 applicaiton.yml 中的
# JWT jwt: header: Authorization secret: mySecret expiration: 604800 tokenHead: "Bearer " route: authentication: path: auth refresh: refresh register: "auth/register"
同樣的 @RequestMapping(value = "${jwt.route.authentication.path}", method = RequestMethod.POST) 中的 jwt.route.authentication.path 也是定義在上面的
@RestController
public class AuthController {
@Value("${jwt.header}")
private String tokenHeader;
@Autowired
private AuthService authService;
@RequestMapping(value = "${jwt.route.authentication.path}", method = RequestMethod.POST)
public ResponseEntity<?> createAuthenticationToken(
@RequestBody JwtAuthenticationRequest authenticationRequest) throws AuthenticationException{
final String token = authService.login(authenticationRequest.getUsername(), authenticationRequest.getPassword());
// Return the token
return ResponseEntity.ok(new JwtAuthenticationResponse(token));
}
@RequestMapping(value = "${jwt.route.authentication.refresh}", method = RequestMethod.GET)
public ResponseEntity<?> refreshAndGetAuthenticationToken(
HttpServletRequest request) throws AuthenticationException{
String token = request.getHeader(tokenHeader);
String refreshedToken = authService.refresh(token);
if(refreshedToken == null) {
return ResponseEntity.badRequest().body(null);
} else {
return ResponseEntity.ok(new JwtAuthenticationResponse(refreshedToken));
}
}
@RequestMapping(value = "${jwt.route.authentication.register}", method = RequestMethod.POST)
public User register(@RequestBody User addedUser) throws AuthenticationException{
return authService.register(addedUser);
}
}
驗(yàn)證時(shí)間
接下來(lái),我們就可以看看我們的成果了,首先注冊(cè)一個(gè)用戶 peng2,很完美的注冊(cè)成功了

注冊(cè)用戶
然后在 /auth 中取得token,也很成功

取得token
不使用token時(shí),訪問(wèn) /users 的結(jié)果,不出意料的失敗,提示未授權(quán)。

不使用token訪問(wèn)users列表
使用token時(shí),訪問(wèn) /users 的結(jié)果,雖然仍是失敗,但這次提示訪問(wèn)被拒絕,意思就是雖然你已經(jīng)得到了授權(quán),但由于你的會(huì)員級(jí)別還只是普卡會(huì)員,所以你的請(qǐng)求被拒絕。

image_1bas22va52vk1rj445fhm87k72a.png-156.9kB
接下來(lái)我們?cè)L問(wèn) /users/?username=peng2,竟然可以訪問(wèn)啊

訪問(wèn)自己的信息是允許的
這是由于我們?yōu)檫@個(gè)方法定義的權(quán)限就是:擁有ADMIN角色或者是當(dāng)前用戶本身。Spring Security真是很方便,很強(qiáng)大。
@PostAuthorize("returnObject.username == principal.username or hasRole('ROLE_ADMIN')")
@RequestMapping(value = "/",method = RequestMethod.GET)
public User getUserByUsername(@RequestParam(value="username") String username) {
return repository.findByUsername(username);
}
本章代碼: https://github.com/wpcfan/spring-boot-tut/tree/chap04
以上所述是小編給大家介紹的Spring Boot(四)之使用JWT和Spring Security保護(hù)REST API,希望對(duì)大家有所幫助,如果大家有任何疑問(wèn)請(qǐng)給我留言,小編會(huì)及時(shí)回復(fù)大家的。在此也非常感謝大家對(duì)腳本之家網(wǎng)站的支持!
相關(guān)文章
spring AOP代理執(zhí)行@EnableAspectJAutoProxy的exposeProxy屬性詳解
這篇文章主要為大家介紹了spring AOP代理執(zhí)行@EnableAspectJAutoProxy的exposeProxy屬性詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-09-09
spring cloud將spring boot服務(wù)注冊(cè)到Eureka Server上的方法
本篇文章主要介紹了spring cloud將spring boot服務(wù)注冊(cè)到Eureka Server上的方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-01-01
java ant包中的org.apache.tools.zip實(shí)現(xiàn)壓縮和解壓縮實(shí)例詳解
這篇文章主要介紹了java ant包中的org.apache.tools.zip實(shí)現(xiàn)壓縮和解壓縮實(shí)例詳解的相關(guān)資料,需要的朋友可以參考下2017-04-04
詳解Spring MVC的異步模式(高性能的關(guān)鍵)
本篇文章主要介紹了詳解Spring MVC的異步模式(高性能的關(guān)鍵),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-02-02
基于ThreadLocal 的用法及內(nèi)存泄露(內(nèi)存溢出)
這篇文章主要介紹了基于ThreadLocal 的用法及內(nèi)存泄露(內(nèi)存溢出),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-10-10

