文章目錄
- 前言
- 一、Spring Security是什麼?
-
- 1.認證
-
- 1.1基于Session認證
- 1.2基于Token認證
- 2.授權
- 3.基于角色通路控制
- 二、OAuth2.0是什麼?
- 三、JWT是什麼?
- 四、代碼實作
-
- 1.建立Spring Boot項目
- 2.Spring Security
-
- 2.1基本使用
- 2.2自定義功能
-
- 2.2.1內建資料庫
- 2.3通過注解權限控制
- 3.OAuth2.0
-
- 3.1基本使用
- 3.2四種授權模式
-
- 3.2.1 code碼授權
- 3.2.2 靜默授權
- 3.2.3 用戶端授權
- 3.2.4 密碼授權
- 4.JWT
-
- 4.1 基本使用
-
- 4.1.1 token過濾器
- 4.1.2 WebSecurityConfig
- 4.1.3 AuthorizationJwtServerConfig
- 4.1.4 登出
前言
基于Spring Boot項目使用Spring Security+OAuth2.0+JWT搭建使用者認證中心。
一、Spring Security是什麼?
Spring Security是一個強大且高度可定制的身份驗證和通路控制架構。它是用于保護基于Spring的應用程式的實際标準。Spring Security官網
1.認證
1.1基于Session認證
使用者登入成功後,會建立一個session儲存在伺服器端,session id儲存在cookie中
1.2基于Token認證
使用token作為唯一辨別
2.授權
3.基于角色通路控制
二、OAuth2.0是什麼?
OAuth 2.0 是授權的行業标準協定。OAuth 2.0 側重于用戶端開發人員的簡單性,同時為 Web 應用程式、桌面應用程式、行動電話和客廳裝置提供特定的授權流。該規範及其擴充正在IETF非授權工作組内制定。
三、JWT是什麼?
JSON Web Token (JWT) 是一個開放标準(RFC 7519),它定義了一種緊湊且自成一體的方式,将各方之間安全傳輸資訊作為 JSON 對象。此資訊可以進行驗證和信任,因為它是以數字方式簽名的。JWTs 可以使用秘密(使用HMAC算法)或使用RSA或ECDSA的公鑰/私鑰對進行簽名。
四、代碼實作
1.建立Spring Boot項目
建立一個Spring Boot項目然後通過maven引入所需依賴就可以。需要注意的是不同版本的Spring Boot和依賴包可能會出現方法過時等問題。本demo的Spring Boot的版本是2.4.4。下面是所有依賴包。
<!-- 核心依賴 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- 測試依賴 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- mybatis-plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.2</version>
</dependency>
<!-- mysql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.22</version>
</dependency>
<!-- 資料源驅動包 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.4</version>
</dependency>
<!-- hutool -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.5.8</version>
</dependency>
<!-- jwt -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>1.1.1.RELEASE</version>
</dependency>
<!-- jwt工具 -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.3.0</version>
</dependency>
<!--Spring boot Redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 日志 -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<!-- oauth2.0 -->
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.3.6.RELEASE</version>
</dependency>
2.Spring Security
2.1基本使用
在引入Spring Security依賴之後啟動項目,通路http://localhost:8080/login,Spring Security自帶了一個登陸頁面。如圖
這就說明Spring Security使用成功。使用者名預設是user,密碼在啟動項目時會顯示在控制台。
接下來我們對Spring Security進行改造,實作一些我們自己的需求。
2.2自定義功能
2.2.1內建資料庫
資料庫連接配接省略
1.建立配置類
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) // 開啟方法級别安全
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private UserDetailsServiceImpl userDetailsService;
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 一些簡單的配置
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
// 禁用session
http.sessionManagement().disable();
http.csrf().disable();
http.formLogin()
// 自定義登入成功後路徑
.defaultSuccessUrl("/hello")
// 自定義登入路徑
.loginProcessingUrl("/doLogin");
// 可以匿名通路
http.authorizeRequests()
.antMatchers("/doLogin").anonymous()
.anyRequest().authenticated();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 使用者資料從資料庫擷取
auth.userDetailsService(userDetailsService);
}
}
2.自定義身份認證,建立一個類實作UserDetailsService
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Resource
private BCryptPasswordEncoder bCryptPasswordEncoder;
@Resource
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
/* 使用者在登入時,會進入此方法(在配置類中進行配置),參數是使用者名,這裡使用了mybatisplus
* 做了一個簡單的通過使用者名查詢使用者,springsecurity會自動對密碼進行比對
*/
QueryWrapper<com.springboot.demo.security.entity.User> wrapper = new QueryWrapper<>();
wrapper.eq("username", s);
com.springboot.demo.security.entity.User sysUser = userMapper.selectOne(wrapper);
String password = sysUser.getPassword();
List<GrantedAuthority> userList = new ArrayList<>();
// userList是權限集合,這裡也是做一個簡單的權限添加
userList.add(new SimpleGrantedAuthority("add"));
// springsecurity5.0後密碼需要加密一次,不然會報錯
return new User("user", bCryptPasswordEncoder.encode(password), userList);
}
3.通路登入頁進行測試
登入成功後跳轉到自己配置的登入成功頁面
2.3通過注解權限控制
使用注解進行權限控制首先需要在配置類中加上注解@EnableGlobalMethodSecurity(prePostEnabled = true),表示開啟方法級安全級别。有三種機制,這裡使用@PreAuthorize注解,在上述登入中我們把add這個權限配置設定給了使用者,是以登入後直接通路的hello沒有權限限制可以直接通路
@GetMapping(value = "/hello")
public String hello() {
return "hello";
}
@PreAuthorize("hasAuthority('add')")
@GetMapping(value = "/add")
public String add() {
return "add";
}
@PreAuthorize("hasAuthority('del')")
@GetMapping(value = "/del")
public String del() {
return "del";
}
有add的權限也可以通路add
通路del就會提示授權異常
springsecurity的簡單應用就大功告成。
3.OAuth2.0
3.1基本使用
首先引入所需maven依賴,然後我們建立一個配置類AuthorizationJwtServerConfig內建AuthorizationServerConfigurerAdapter,先使用redis做token存儲。
@Configuration
@EnableAuthorizationServer
public class AuthorizationJwtServerConfig extends AuthorizationServerConfigurerAdapter {
@Resource
private RedisConnectionFactory redisConnectionFactory;
@Bean
public TokenStore tokenStore() {
return new RedisTokenStore(redisConnectionFactory);
}
/**
* 暴露授權服務
* @param endpoints
* @throws Exception
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(tokenStore());
}
}
然後啟動項目,我們可以在控制台中看到列印的内容,說明oauth2.0可以使用。
3.2四種授權模式
3.2.1 code碼授權
通過code碼換取token。
在AuthorizationJwtServerConfig配置類添加配置。
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
// 第三方應用用戶端id,相當于賬号,可自定義
.withClient("web")
// 第三方應用密碼,需要加密,相當于密碼,可自定義
.secret(new BCryptPasswordEncoder().encode("web"))
// 第三方作用域,自定義
.scopes("read")
// 授權類型,使用code碼
.authorizedGrantTypes("authorization_code")
// 有效時間
.accessTokenValiditySeconds(7200)
// 重定向url,必須是公網位址,必須是https
.redirectUris("https://www.baidu.com");
}
重新開機項目後我們在浏覽器上通路http://localhost:8080/oauth/authorize?response_type=code&scope=read&client_id=web&redirect_uri=https://www.baidu.com
response_type參數是授權類型code是code碼授權,scope作用域對應代碼配置中的作用域值,client_id也是對應代碼中的配置,redirect_uri同理。
回車之後會跳轉到登入頁面,然後輸入賬号密碼再次登入,會跳轉到授權頁面
确認授權後會跳轉到我們配置的重定向位址,并且獲得了code碼
有了code碼之後我們就要用code碼去換取token,使用工具postman發送一個post請求
除此之外還需要配置Authorization,類型選擇Basic Auth,賬号密碼是代碼中配置的
發送請求之後就會獲得token了
3.2.2 靜默授權
直接擷取token,代碼跟code碼授權沒有太大差別,類型換成implicit靜默授權就可以了
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
// 第三方應用用戶端id
.withClient("app")
// 第三方應用密碼,需要加密
.secret(new BCryptPasswordEncoder().encode("app"))
// 第三方作用域
.scopes("read")
// 授權類型
.authorizedGrantTypes("implicit")
.accessTokenValiditySeconds(7200)
.redirectUris("https://www.baidu.com");
}
重新開機項目,浏覽器通路http://localhost:8080/oauth/authorize?response_type=token&scope=read&client_id=app&redirect_uri=https://www.baidu.com
注意這裡的response_type的值是token,直接通路也會跳轉到登入頁面,登入成功後進行授權
授權成功後,token會顯示在浏覽器上
3.2.3 用戶端授權
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
// 第三方應用用戶端id
.withClient("client")
// 第三方應用密碼,需要加密
.secret(new BCryptPasswordEncoder().encode("client"))
// 第三方作用域
.scopes("read")
// 授權類型
.authorizedGrantTypes("client_credentials")
.accessTokenValiditySeconds(7200)
.redirectUris("https://www.baidu.com");
}
使用postman發送post請求擷取token,隻需grant_type一個參數
3.2.4 密碼授權
首先更改WebSecurityConfig配置類,增加一個認證管理器
/**
* 用密碼模式授權認證管理器
* @return
* @throws
*/
@Override
@Bean
public AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
AuthorizationJwtServerConfig類中把AuthenticationManager注入進來,暴露授權服務中加入認證管理器
@Resource
private AuthenticationManager authenticationManager;
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
// 第三方應用用戶端id
.withClient("qq")
// 第三方應用密碼,需要加密
.secret(new BCryptPasswordEncoder().encode("qq"))
// 第三方作用域
.scopes("read")
// 授權類型
.authorizedGrantTypes("password")
.accessTokenValiditySeconds(7200)
.redirectUris("https://www.baidu.com");
}
/**
* 暴露授權服務
* @param endpoints
* @throws Exception
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(tokenStore()).authenticationManager(authenticationManager);
}
重新開機項目後使用postman來測試,發送post請求
Authorization也需要配置
成功後直接擷取token
4.JWT
4.1 基本使用
使用jwt生成token,并做token的驗證
4.1.1 token過濾器
@Configuration
public class JwtTokenFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
String path = httpServletRequest.getRequestURI();
String method = httpServletRequest.getMethod();
// 對于登入直接放行
if ("/login".equals(path) && "POST".equals(method)) {
filterChain.doFilter(httpServletRequest, httpServletResponse);
return;
}
// 擷取token并驗證
String authorization = httpServletRequest.getHeader("Authorization");
if (!StrUtil.hasBlank(authorization)) {
String jwt = authorization.replaceAll("bearer ", "");
// 建立一個token解析器(test作為jwt生成token的簽名是自定義的,一般是作為配置固定值)
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("test")).build();
DecodedJWT decodedJwt;
try {
decodedJwt = jwtVerifier.verify(jwt);
} catch (Exception e) {
httpServletResponse.getWriter().write("token驗證失敗");
return;
}
// 擷取使用者名,密碼,角色權限
String username = decodedJwt.getClaim("username").asString();
String password = decodedJwt.getClaim("password").asString();
List<String> roles = decodedJwt.getClaim("role").asList(String.class);
List<SimpleGrantedAuthority> roleList = new ArrayList<>();
roles.forEach(role -> {
roleList.add(new SimpleGrantedAuthority(role));
});
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
new UsernamePasswordAuthenticationToken(username, password, roleList);
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
filterChain.doFilter(httpServletRequest, httpServletResponse);
return;
}
httpServletResponse.getWriter().write("token驗證失敗");
}
}
4.1.2 WebSecurityConfig
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private UserDetailsServiceImpl userDetailsService;
@Resource
private JwtTokenFilter jwtTokenFilter;
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 一些簡單的配置
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
// 登入之前驗證token
http.addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class);
// 禁用session
http.sessionManagement().disable();
http.csrf().disable();
http.formLogin()
// 登入成功處理器
.successHandler(authenticationSuccessHandler())
// 登入失敗處理器
.failureHandler(authenticationFailureHandler());
// 除了登入可以匿名通路
http.authorizeRequests()
.antMatchers("/login").anonymous()
.anyRequest().authenticated();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 使用者資料從資料庫擷取
auth.userDetailsService(userDetailsService);
}
/**
* 登入成功處理器
*/
@Bean
public AuthenticationSuccessHandler authenticationSuccessHandler() {
return ((httpServletRequest, httpServletResponse, authentication) -> {
httpServletResponse.setContentType("application/json;charset=utf-8");
User user = (User)authentication.getPrincipal();
// 使用者名
String username = user.getUsername();
// 密碼
String password = user.getPassword();
// 權限
Collection<GrantedAuthority> grantedAuthorities = user.getAuthorities();
List<String> roleList = new ArrayList<>();
grantedAuthorities.forEach(grantedAuthority -> {
roleList.add(grantedAuthority.getAuthority());
});
String[] roles = new String[roleList.size()];
// 用jwt生成token
HashMap<String, Object> headMap = new HashMap<>(16);
// 使用的算法
headMap.put("alg", "HS256");
headMap.put("typ", "JWT");
Date nowDate = new Date();
// 過期時間可以自定義
Date expDate = new Date(nowDate.getTime() + 2 * 60 * 60 * 1000);
String jwt = JWT.create().withHeader(headMap)
.withIssuedAt(nowDate)
.withExpiresAt(expDate)
// 主題,自定義
.withSubject("demo")
.withClaim("username", username)
.withClaim("password", password)
.withArrayClaim("role", roleList.toArray(roles))
// 簽名,自定義,同一個項目中簽名是唯一
.sign(Algorithm.HMAC256("test"));
// 儲存token到redis
redisTemplate.opsForValue().set("token:" + jwt, user, 7200);
// 傳回token
HashMap<String, Object> hashMap = new HashMap<>(16);
hashMap.put("username", username);
hashMap.put("create_time", nowDate);
hashMap.put("expires_time", expDate);
hashMap.put("access_token", jwt);
hashMap.put("type", "bearer");
ObjectMapper objectMapper = new ObjectMapper();
String s = objectMapper.writeValueAsString(hashMap);
PrintWriter printWriter = httpServletResponse.getWriter();
printWriter.write(s);
printWriter.flush();
printWriter.close();
});
}
/**
* 登入失敗處理器
*/
@Bean
public AuthenticationFailureHandler authenticationFailureHandler() {
return ((httpServletRequest, httpServletResponse, e) -> {
HashMap<String, Object> hashMap = new HashMap<>(16);
hashMap.put("error", e);
hashMap.put("message", "登入失敗");
ObjectMapper objectMapper = new ObjectMapper();
String s = objectMapper.writeValueAsString(hashMap);
PrintWriter printWriter = httpServletResponse.getWriter();
printWriter.write(s);
printWriter.flush();
printWriter.close();
});
}
}
4.1.3 AuthorizationJwtServerConfig
@Configuration
@EnableAuthorizationServer
public class AuthorizationJwtServerConfig extends AuthorizationServerConfigurerAdapter {
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
/**
* jwt token轉換器
* @return
*/
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
// 簽名
jwtAccessTokenConverter.setSigningKey("test");
return jwtAccessTokenConverter;
}
/**
* 暴露授權服務
* @param endpoints
* @throws Exception
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.accessTokenConverter(jwtAccessTokenConverter());
}
}
然後啟動項目,使用postman測試
登入擷取token
使用token通路接口
4.1.4 登出
登出就相當于是删除redis緩存的token
@PostMapping(value = "/doLogout")
public Object logout() {
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
String head = request.getHeader("Authorization");
if (!StrUtil.isBlank(head)) {
String jwt = head.replaceAll("bearer ", "");
if (!StrUtil.hasBlank(jwt)) {
redisTemplate.delete("token:" + jwt);
return "登出成功";
}
}
return "登出失敗";
}