Spring Security並結合JWT實現用戶認證(Authentication) 和用戶授權(Authorization)


引言
在Web應用開發中,安全一直是非常重要的一個方面。Spring Security基於Spring 框架,提供了一套Web應用安全性的完整解決方案。

JwT (JSON Web Token) 是當前比較主源的Token令牌生成方案,非常適合作為登錄和授權認證的憑證。

這里我們就使用 Spring Security並結合JWT實現用戶認證(Authentication) 和用戶授權(Authorization) 兩個主要部分的安全內容。

一、JWT與OAuth2的區別
在此之前,只是停留在用的階段,對二者的使用場景很是模糊,感覺都是一樣的呀,有啥不同呢,這里我也是根據網上的指點,在這羅列一下。

1、跨域實現不同
首先是涉及到跨域的問題:
如果ABC三個系統是相同域名的,比如都是www.a.com,那么就可以使用JWT的方式,將三個系統改造成統一的一個登錄和攔截校驗。

如果ABC不是相同域名的,比如:
www. a.com,www.b.com,www.c.com,建議不要使用JWT這種方式,因為需要涉及到跨域,這樣跨域獲取token可能存在安全問題,可以考慮使用傳統cookie+session方式來實現跨域的SSO機制。或者可以使用SpringBoot+Security+OAuth2來實現,這就涉及到了OAuth2了.

2、所屬性質原理不同
OAuth2是一種授權框架

OAuth2是一種授權框架,提供了一套詳細的授權機制(指導)。用戶或應用可以通過公開的或私有的設置,授權第三方應用訪問特定資源。

JWT是一種認證協議

JWT提供了一種用於發布接入令牌(Access Token),並對發布的簽名接入令牌進行驗證的方法。 令牌(Token)本身包含了一系列聲明,應用程序可以根據這些聲明限制用戶對資源的訪問。

3、應用場景不同
OAuth2用在使用第三方賬號登錄的情況(比如使用weibo, qq, github登錄某個app)

JWT是用在前后端分離, 需要簡單的對后台API進行保護時使用.(前后端分離無session, 頻繁傳用戶密碼不安全)

OAuth2是一個相對復雜的協議, 有4種授權模式, 其中的access code模式在實現時可以使用jwt才生成code, 也可以不用. 它們之間沒有必然的聯系;
OAuth2有client和scope的概念,jwt沒有。
如果只是拿來用於頒布token的話,二者沒區別,常用的bearer算法oauth、jwt都可以用,只是應用場景不同而已。

具體的比較推薦給大家一片文章,寫的很詳細
https://blog.csdn.net/A15712399740/article/details/95233903

二、正題Spring Security並結合JWT實現用戶認證和用戶授權
2.1、添加pom.xml依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
1
2
3
4
5
6
7
8
9
2.2、添加配置文件WebSecurityConfig
在config 包下新建一個Spring Security 的配置類WebSecurityConfig, 主要是進行一些安全相關的配置,比如權限URL匹配策略、認證過濾器配置、定制身份驗證組件、開啟權限認證注解等,具體代碼作用參見代碼注釋。

/**
* @program: mangocms
* @description: 安全配置類
* @author: zjc
* @create: 2020-08-05 19:53
**/
@Configuration
@EnableWebSecurity //開啟Spring Security
@EnableGlobalMethodSecurity(prePostEnabled = true) //開啟權限注解 例如:@PreAuthorize注解
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//使用自定義身份認證組件
auth.authenticationProvider(new JwtAuthenticationProvider(userDetailsService));
}

@Override
protected void configure(HttpSecurity http) throws Exception {
//禁用csrf,由於使用的是jwt,我們這里不需要csrf
http.cors().and().csrf().disable().authorizeRequests()
//跨域預檢請求
.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
//web jars
.antMatchers("/webjars/**").permitAll()
//查看SQL監控(druid)
.antMatchers("/druid/**").permitAll()
//首頁和登錄頁面
.antMatchers("/").permitAll().antMatchers("/login").permitAll()
//swagger
.antMatchers("/swagger-ui.html").permitAll()
.antMatchers("/swagger-resources/**").permitAll()
.antMatchers("/v2/api-docs").permitAll()
.antMatchers("/webjars/springfox-swagger-ui/**").permitAll()
//驗證碼
.antMatchers("/captcha.jpg**").permitAll()
//服務監控
.antMatchers("/actuator/**").permitAll()
//其他所有請求需要身份驗證
.anyRequest().authenticated();
//退出登錄處理器
http.logout().logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler());
//token驗證過濾器
http.addFilterBefore(new JwtAuthenticationFilter(authenticationManager()),UsernamePasswordAuthenticationFilter.class);


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
2.3、JwtAurthenticationFilter登錄認證過濾器
登錄認證過濾器負責登錄認證時檢查並生產令牌並保存到上下文,接口權限認證過程時,系統從上下文獲取令牌校驗接口訪問權限,新建一個security包,在其下創建JwtAurthenticationFilter並繼承BasicAuthenticationFilter, 覆寫其中的doFilterlntermal 方法進行Token校驗。

/**
* @program: mangocms
* @description: 登錄認證過濾器
* @author: zjc
* @create: 2020-08-05 20:53
**/
@Configuration
public class JwtAuthenticationFilter extends BasicAuthenticationFilter {
@Autowired
public JwtAuthenticationFilter(AuthenticationManager authenticationManager){
super(authenticationManager);
}

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
//獲取token,並檢查登陸狀態,檢查request中的請求信息
SecurityUtils.checkAuthentication(request);
chain.doFilter(request,response);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
這里我們把驗證邏輯抽取到了SecurityUtils 的checkAuthentication 方法中,checkAuthentication通過JwtTokenUtils的方法獲取認證信息並保存到Spring Security上下文中。

2.4、SecurityUtils獲取令牌並進行認證
/**
* 獲取令牌進行認證
*/
public static void checkAuthentication(HttpServletRequest request){
//獲取令牌並根據令牌獲取登錄認證信息
Authentication authentication =JwtTokenUtils.getAuthentticattionFromToken(request);
//設置登錄認證信息到上下文
SecurityContextHolder.getContext().setAuthentication(authentication);
}
1
2
3
4
5
6
7
8
9
2.5、JwtTokenUtils根據請求令牌獲取登錄認證信息
這里只是再進行主要方法的邏輯追蹤,后面我會附上完整的代碼下載地址。

/**
* 根據請求令牌獲取登錄認證信息
*
*/
public static Authentication getAuthentticattionFromToken(HttpServletRequest request){
Authentication authentication =null;
//獲取請求攜帶的令牌
String token = JwtTokenUtils.getToken(request);
if (token != null){
//請求令牌並不能為空
if(SecurityUtils.getAuthentication() == null){
//上下文中的Authentication
Claims claims = getClaimsFromToken(token);
if(claims == null){
return null;
}
String username =claims.getSubject();
if(username == null){
return null;
}
if(isTokenExpired(token)){
return null;
}
Object authors =claims.get(AUTHORITIES);
List<GrantedAuthority> authorities =new ArrayList<>();
if(authors != null && authors instanceof List){
for(Object object : (List) authors){
authorities.add(new GrantedAuthorityImpl(
(String)((Map) object).get("authority")));
}
}
authentication = new JwtAuthenticatioToken(username,null,authorities,token);
}else{
if(validateToken(token,SecurityUtils.getUsername())){
//如果上下文中Authentication非空,且請求命令合法
//直接返回當前登錄認證信息
authentication = SecurityUtils.getAuthentication();
}
}
}
return authentication;
}

/**
* 獲取請求Token
* @param request
* @return
*/
//嘗試從請求頭中獲取請求寫帶的令牌,默認從請求頭中的“Authentication”參數以“Bearer”開頭的信息為令牌信息,
//若為空的話,嘗試從token參數獲取
public static String getToken(HttpServletRequest request){
String token = request.getHeader("Authorization");
String tokenHead = "Bearer";
if(token == null){
token = request.getHeader("token");
}else if(token.contains(tokenHead)){
//當且僅當此字符串包含指定的tokenHead值序列時,返回true。
token =token.substring(tokenHead.length());
}
if("".equals(token)){
token = null;
}
return token;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
2.6身份認證組件
SpringSecurity 的登錄驗證是交由ProviderManager負責的,ProviderManager 在實際驗證時其通過調用AuthenticationProvider的authenticate方法來進行認證。數據庫類型的默認實現方案是DaoAuthenticationProvider。我們這里通過繼承DaoAuthenticationProvider 定制默認的登錄認證邏輯,在Security 包下新建驗證器JwtAuthenticationProvider並繼承DaoAuthenicationProvider,覆蓋實現additionalAuthenticationChecks方法進行密碼匹配,我們這里沒有使用默認的密碼認證器 (我們使用鹽salt來對密碼加密,默認密碼驗證器沒有加鹽),所以這里定制了自己的密碼校驗邏輯, 當然你也可以通過直接覆寫authenticate方法來完成更大范圍的登錄認證需求定制。

JwtAuthenticationProvider身份驗證提供者

/**
* @program: mangocms
* @description: 身份認證提供者
* @author: zjc
* @create: 2020-08-08 09:35
**/

public class JwtAuthenticationProvider extends DaoAuthenticationProvider {
public JwtAuthenticationProvider(UserDetailsService userDetailsService)
{
setUserDetailsService(userDetailsService);
}

@Override
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
if(authentication.getCredentials() == null){
logger.debug("Authentraction failed: no credentials provided");
throw new BadCredentialsException(messages.getMessage("AbstractUserDetalisAuthenticationProvider.badCredentials","Bad credentials"));
}
String presentedPassword = authentication.getCredentials().toString();
String salt = ((JwtUserDetails)userDetails).getSalt();
if(! new PasswordEncoder(salt).matches(userDetails.getPassword(),presentedPassword)){
//覆寫密碼驗證邏輯 matches:匹配兩者
logger.debug("Authentication failed: password does not match");
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"
));
}
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
2.7、認證信息查詢
我們上面提到登錄認證默認是通過DaoAuthenticationProvider來完成登錄認證的,而我們知道登錄驗證器在進行時肯定是要從數據庫獲取用戶信息進行匹配的,而這個獲取用戶信息的任務是通過Spring Security的UserDetailsService組件來完成的。

在security包下新建一個UserDetailsServiceImpl並實現UserDetailsService接口,覆寫其中的方法lodUserByUsermame,查詢用戶的密碼信息和權限信息並封裝到UseDetailis的實現類對象,作為結果JwtUserDetails返回給DaoAuthenticationProvider做進一步處理。

新建一個UserDetailsServiceImpl並實現UserDetailsService接口
/**
* @program: mangocms
* @description: UserDetails實現類
* @author: zjc
* @create: 2020-08-08 10:20
**/
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserService userService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userService.findByName(username);
if (user == null){
throw new UsernameNotFoundException("該用戶不存在");
}
//用戶權限列表,根據權限標志如@PreAuthorize("hasAuthority('sys:menu:view'))
//標注的接口對比,決定是否可以調用該接口
Set<String> permissions = userService.findPermissions(user.getName());
List<GrantedAuthority> grantedAuthorities = permissions.stream().map(GrantedAuthorityImpl::new).collect(Collectors.toList());
return new JwtUserDetails(user.getName(),user.getPassword(),user.getSalt(),grantedAuthorities);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
創建安全用戶模型JwtUserDetails

/**
* @program: mangocms
* @description: 安全用戶模型
* @author: zjc
* @create: 2020-08-08 10:42
**/
public class JwtUserDetails implements UserDetails {
private String username;
private String password;
private String salt;
private Collection<? extends GrantedAuthority> authorities;

public JwtUserDetails(String username, String password, String salt, Collection<? extends GrantedAuthority> authorities) {
this.username = username;
this.password = password;
this.salt = salt;
this.authorities = authorities;
}
//忽略set/get
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
GrantedAuthorityimpl實現Spring Security的GrantedAuthority接口,是對權限的封裝,內部包含一個字符串類類的權限標識authority,對應菜單表的perms字段的權限字符串,比如用戶管理的增刪改查權限標志sys:user:view、 sys:user:add、 sys:user:edit、 sys:user:delete等


/**
* @program: mangocms
* @description: 權限封裝
* @author: zjc
* @create: 2020-08-08 10:46
**/

public class GrantedAuthorityImpl implements GrantedAuthority {
private static final long serialVersionUID = 1L;
private String authority;

public GrantedAuthorityImpl(String authority) {
this.authority = authority;
}

public void setAuthority(String authority){
this.authority = authority;
}
@Override
public String getAuthority() {
return this.getAuthority();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2.8、添加權限注解
在用戶擁有某個后台接口訪問權限的時候才能訪問,這叫作接口保護。我們這里就通過Spring Security提供的權限注解來保護后台接口免受非法訪問,這里以字典管理模塊為例,其他模塊同理。

在SysDictController 的接口上添加類似@PreAuthorize("hasAuthority(sysdictwiew/)的注解,表示只有當前登錄用戶擁有sys:dict:view權限標識才能訪向此按口,這里的權限標識需對應菜單表中的perms權限信息,所以可以通過配置菜單表的權限來靈活控制接口的訪問權限。

/**
* 根據名稱查詢
*
* @param lable
* @return
*/
@PreAuthorize("hasAuthority('sys:dict:view')")
@PostMapping(value = "/findByLable")
@ApiOperation(value ="根據名稱查詢",httpMethod = "POST",response = HttpResult.class,notes = "按名稱查詢")
public HttpResult findByLable(@RequestParam String lable) {
return HttpResult.ok(dictService.findByLable(lable));
}

1
2
3
4
5
6
7
8
9
10
11
12
13
2.9、為Swagger添加令牌參數
由於我們引入Spring Securitry安全框架,接口受到保護,需要攜帶合法的token令牌,就是登錄成功之后由后台返回才能正常訪問,但是Swagger本身的接口測試頁面默認並沒有傳token參數的地方,因此需要簡單定制一下,修改SwaggerConfig配類即可。

/**
* @program: mango
* @description: swagger配置類
* @author: zjc
* @create: 2020-07-18 09:51
**/
@Configuration
@EnableSwagger2
public class SwaggerConfig {
/**
* 創建API應用
* apiInfo() 增加API相關信息
* 通過select()函數返回一個ApiSelectorBuilder實例,用來控制哪些接口暴露給Swagger來展現,
* 本例采用指定掃描的包路徑來定義指定要建立API的目錄。
*
* @return
*/
@Bean
public Docket createRestApi() {
//添加請求參數,我們這里吧token作為請求頭部參數傳入后端
/* ParameterBuilder parameterBuilder = new ParameterBuilder();
List<Parameter> parameters = new ArrayList<>();
parameterBuilder.name("token").description("令牌").modelRef(new ModelRef("string")).parameterType("header").required(false).build();
parameters.add(parameterBuilder.build());*/


ParameterBuilder ticketPar = new ParameterBuilder();
List<Parameter> pars = new ArrayList<Parameter>();
ticketPar.name("token").description("user ticket")//Token 以及Authorization 為自定義的參數,session保存的名字是哪個就可以寫成那個
.modelRef(new ModelRef("string")).parameterType("header")
.required(false).build(); //header中的ticket參數非必填,傳空也可以
pars.add(ticketPar.build()); //根據每個方法名也知道當前方法在設置什么參數

return new
Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.any())
.paths(PathSelectors.any())
.build()
.globalOperationParameters(pars);
}

/**
* 創建該API的基本信息(這些基本信息會展現在文檔頁面中)
* 訪問地址:http://項目實際地址/swagger-ui.html
*
* @return
*/
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("ManGo接口頁面")
.description("所有接口信息")
.version("1.0")
.build();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
這回我們在Swagger界面就能夠看到輸入令牌的地方了。


三、登錄接口實現
在登錄控制器中添加一個登錄接口login, 在其中驗證驗證碼、用戶名、密碼信息。匹配成功之后,執行Spring Security的登錄認證機制。登錄成功之后,返回Token令牌憑證。

/**
* 登錄入口
* @param loginBean
* @param request
* @return
* @throws Exception
*/
@PostMapping(value = "/login")
public HttpResult login(@RequestBody LoginBean loginBean, HttpServletRequest request)throws Exception{
String username = loginBean.getAccount();
String password = loginBean.getPassword();
String captcha = loginBean.getCaptcha();
//從session中獲取之前保存的驗證碼,跟前台傳過來的驗證碼進行匹配
Object kaptcha = request.getSession().getAttribute(Constants.KAPTCHA_SESSION_KEY);
if(kaptcha == null){
return HttpResult.error("驗證碼已失效");
}
if(!captcha.equals(kaptcha)){
return HttpResult.error("驗證碼不正確");
}
//用戶信息
User user = userService.findByName(username);
if(user == null){
return HttpResult.error("帳號不存在");
}
if (!PasswordUtils.matches(user.getSalt(),password,user.getPassword())){
return HttpResult.error("密碼不正確");
}
//賬號鎖定
if(user.getStatus() == 0){
return HttpResult.error("帳號已被鎖定,請聯系管理員");
}
//系統登陸認證
JwtAuthenticatioToken token = SecurityUtils.login(request,username,password,authenticationManager);
return HttpResult.ok(token);

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
我們這里將Spring Security的登錄認證邏輯封裝到了工具類SecurityUtils的login方法中,認證流程大致分為下面4個步驟:

(1)將用戶名密碼的認證信息封裝到JwtAuthenticatioToken 對象。
(2)通過調用authenticationManager.authenticate(token)執行認證流程。
(3)通過SecurityContextHolder將認證信息保存到Security上下文。
(4)通過JwtTokenUtils generateToken(authentication)生成token並返回。
具體過程詳見代碼。

/**
* 系統登錄認證
* @param request
* @param username
* @param password
* @param authenticationManager
* @return
*/
public static JwtAuthenticatioToken login(HttpServletRequest request, String username, String password, AuthenticationManager authenticationManager) {
JwtAuthenticatioToken token = new JwtAuthenticatioToken(username, password);
token.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// 執行登錄認證過程
Authentication authentication = authenticationManager.authenticate(token);
// 認證成功存儲認證信息到上下文
SecurityContextHolder.getContext().setAuthentication(authentication);
// 生成令牌並返回給客戶端
token.setToken(JwtTokenUtils.generateToken(authentication));
return token;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
JwtTokenUtils生成Token的邏輯

/**
* 生成令牌
*/
public static String generateToken(Authentication authentication) {
Map<String, Object> claims = new HashMap<>(3);
claims.put(USERNAME, SecurityUtils.getUsername(authentication));
claims.put(CREATED, new Date());
claims.put(AUTHORITIES, authentication.getAuthorities());
return generateToken(claims);
}


/**
* 從數據聲明生成令牌
*
* @param claims 數據聲明
* @return 令牌
*/
private static String generateToken(Map<String, Object> claims) {
Date expirationDate = new Date(System.currentTimeMillis() + EXPIRE_TIME);
return Jwts.builder().setClaims(claims).setExpiration(expirationDate).signWith(SignatureAlgorithm.HS512, SECRET).compact();
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
登陸實體LogginBean

public class LoginBean {
private String account;
private String password;
private String captcha;
//省略set/get
}
1
2
3
4
5
6
JwtAuthenticatioToken繼承UsemamePasswordAuthenticationToken,是對令牌信息收封裝,用來作為認證和授權的信任憑證,其中的token信息由JWT負貴生成。
JwtAuthenticatioToken

package com.mango.cms.mangomain.security;

import java.util.Collection;

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;

/**
* 自定義令牌對象
*
*/
public class JwtAuthenticatioToken extends UsernamePasswordAuthenticationToken {

private static final long serialVersionUID = 1L;

private String token;

public JwtAuthenticatioToken(Object principal, Object credentials){
super(principal, credentials);
}

public JwtAuthenticatioToken(Object principal, Object credentials, String token){
super(principal, credentials);
this.token = token;
}

public JwtAuthenticatioToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities, String token) {
super(principal, credentials, authorities);
this.token = token;
}

public String getToken() {
return token;
}

public void setToken(String token) {
this.token = token;
}

public static long getSerialversionuid() {
return serialVersionUID;
}

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
四、測試
接下來就可以運行項目進行測試了,我們點擊驗證碼接口,將生成的驗證碼在輸入到登錄的接口中。

 

這里我數據庫中本是沒有數據的,我只是簡單添加了明文密碼,所以提示密碼錯誤,但這不影響我們代碼的使用。
我已將源碼結構傳到下載界面,大家有需要的可以直接使用。下載地址:
SpringSecurity之JWT實現token認證和授權.zip

這里是一篇更加詳細的使用教程,也推薦給大家,碼字不易,喜歡就點個贊吧!!!!
https://ww.cnblogs.com/xifengxiaoma/p/10020960.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM