OAuth2
OAuth2是當前授權的行業標準,其重點在於爲Web應用程序、桌面應用程序、移動設備以及室內設備的授權流程提供簡單的客戶端開發方式。它爲第三方應用提供對HTTP服務的有限訪問,既可以是資源擁有者通過授權允許第三方應用獲取HTTP服務,也可以是第三方以自己的名義獲取訪問權限。
角色
OAuth2中主要分爲了4種角色:
- Resource Owner(資源所有者),是能夠對受保護的資源授予訪問權限的實體,可以是一個用戶,這時會稱爲終端用戶(end-user)。
- Resource Server(資源服務器),持有受保護的資源,允許持有訪問令牌(Access Token)的請求訪問受保護資源。
- Client(客戶端),持有資源所有者的授權,代表資源所有者對受保護資源進行訪問。
- Authorization Server(授權服務器),對資源所有者的授權進行認證,成功後向客戶端發送訪問令牌。
很多時候,資源服務器和授權服務器是合二爲一的,在授權交互的時候作爲授權服務器,在請求資源交互時作爲資源服務器
Resource Server的配置
Resource Server(可以是授權服務器,也可以是其他的資源服務)提供了受OAuth2保護的資源,這些資源爲API接口、Html頁面、Js文件等.Spring OAuth2提供了實現此保護功能的Spring Security認證過濾器。在加了@Configuration註解的配置類上加@EnableResourceServer註解,開啓Resource Server的功能
JWT
JSON Web Token(JWT)是一種開放的標準(RFC 7519),JWT定義了一種緊湊且自包含的標準,該標準旨在將各個主體的信息包裝爲JSON對象。主體信息是通過數字簽名進行加密和驗證的。常使用HMAC算法或RSA(公鑰/私鑰的非對稱性加密)算法對JWT進行簽名,安全性很高。
- 緊湊性(compact):由於是加密後的字符串,JWT數據體積非常小,可通過POST請求參數或HTTP請求頭髮送。另外,數據體積小意味着傳輸速度很快。
- 自包含(self-contained):JWT包含了主體的所有信息,所以避免了每個請求都需要向Uaa服務驗證身份,降低了服務器的負載。
JWT由3個部分組成,分別以“.”分隔,組成部分如下。
- Header(頭):Header通常由兩部分組成:令牌的類型(即JWT)和使用的算法類型,如HMAC、SHA256和RSA
- Payload(有效載荷):了用戶的一些信息和Claim(聲明、權利)。有3種類型的Claim:保留、公開和私人
- Signature(簽名):需要將Base64編碼後的Header、Payload和密鑰進行簽名
uaa配置
1. 依賴:
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.1.2'
implementation 'org.springframework.cloud:spring-cloud-starter-oauth2:2.2.2.RELEASE'
implementation 'org.springframework.cloud:spring-cloud-starter-consul-discovery:2.2.2.RELEASE'
runtimeOnly 'mysql:mysql-connector-java'
2. 配置文件:
bootstrap.yml:
spring:
application:
name: consul-auth
cloud:
consul:
host: localhost
port: 8500
discovery:
prefer-ip-address: true
service-name: ${spring.application.name}
management:
endpoints:
web:
exposure:
include: "*"
endpoint:
health:
show-details: always
application.yml:
server:
port: 8731
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/ffzs?useUnicode=true&zeroDateTimeBehavior=convertToNull&autoReconnect=true&characterEncoding=utf-8
username: root
password: 123zxc
main:
allow-bean-definition-overriding: true
3. 安裝mysql數據庫
需要通過數據庫進行對人員登錄人員數據進行存儲這裏使用了Mysql數據庫
使用docker安裝:
mysql:
image: mysql
networks:
- spring
restart: always
ports:
- 33060:33060
- 3306:3306
volumes:
- ./mysql/db:/var/lib/mysql
- ./mysql/conf.d:/etc/mysql/conf.d
environment:
- MYSQL_ROOT_PASSWORD=123zxc
command: --default-authentication-plugin=mysql_native_password
創建一個user的表用來存儲人員數據:
CREATE TABLE `myUser` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`password` varchar(255) DEFAULT NULL,
`username` varchar(255) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `name` (`username`)
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8;
BEGIN;
INSERT INTO `myUser` VALUES ('1', '$2a$10$uuFQKbr2q/8aqYlPEBlRw.Z9UrtEPrydIh7IUXaEGVWBowY8mZrUq', 'ffzs'),('2', '$2a$10$QgQ9OtiCMnGzYGPabDzOkeBda0Sb8wqzwnTSErJWPx4GfeNOUvh7q', 'sleepycat');
COMMIT;
添加兩個用戶
4. AuthorizationServer配置
/**
* @author ffzs
* @describe
* @date 2020/6/7
*/
@Configuration
@EnableAuthorizationServer
public class OAuthConfiguration extends AuthorizationServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserDetailsServiceImpl userServiceDetail;
/**
* 配置客戶端信息
* @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("consul_server") // 用戶端id 需要在Authorization Server中是唯一的。
.secret("123456") // 連接密碼
.scopes("server") // 配置的客戶端域爲 service
// .autoApprove(true) // client_secret
// .authorities("ROLE_ADMIN", "ROLE_USER") // 權限信息
.authorizedGrantTypes("implicit", "refresh_token", "password", "authorization_code", "client_credentials") // 類驗證類型
.accessTokenValiditySeconds(60*60); //失效時間
}
/**
* 配置授權Token的節點和Token服務
* @param endpoints
* @throws Exception
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.authenticationManager(authenticationManager) //只有配置了該選項,密碼認證纔會開啓。在大多數情況下都是密碼驗證,所以一般都會配置這個選項
/**
* 需要設置Token的管理策略,目前支持以下3種:
* InMemoryTokenStore:Token存儲在內存中。
* JdbcTokenStore:Token存儲在數據庫中。需要引入spring-jdbc的依賴包,並配置數據源,以及初始化Spring OAuth2的數據庫腳本
* JwtTokenStore:採用JWT形式,這種形式沒有做任何的存儲,因爲JWT本身包含了用戶驗證的所有信息,不需要存儲。採用這種形式,需要引入spring-jwt的依賴
*/
.tokenStore(jwtTokenStore())
.tokenEnhancer(jwtTokenEnhancer())
// .userDetailsService(userServiceDetail) // 配置獲取用戶認證信息的接口
.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST);
}
@Bean
public TokenStore jwtTokenStore() {
return new JwtTokenStore(jwtTokenEnhancer());
}
@Bean
protected JwtAccessTokenConverter jwtTokenEnhancer() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("fanfanzhisu");
return converter;
}
/**
* Token 節點的安全策略
* @param oauthServer
* @throws Exception
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
oauthServer
.tokenKeyAccess("permitAll()")
.checkTokenAccess("isAuthenticated()")
.allowFormAuthenticationForClients()
.passwordEncoder(NoOpPasswordEncoder.getInstance());
}
}
5. WebSecurity 配置
- 開放了actuator路徑供健康檢查
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsServiceImpl userServiceDetail;
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.exceptionHandling()
.authenticationEntryPoint((request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED))
.and()
.authorizeRequests()
.antMatchers("/actuator/**").permitAll()
.antMatchers("/**").authenticated()
.and()
.httpBasic();
}
//
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(userServiceDetail)
.passwordEncoder(new BCryptPasswordEncoder());
}
@Bean
public static NoOpPasswordEncoder passwordEncoder() {
return (NoOpPasswordEncoder) NoOpPasswordEncoder.getInstance();
}
}
Mybatis文件配置:
配置一下model,dao等文件路徑:
@Configuration
@MapperScan("com.ffzs.consulauth.**.dao")
public class MybatisConfig {
@Autowired
private DataSource dataSource;
@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
SqlSessionFactoryBean ssfb = new SqlSessionFactoryBean();
ssfb.setDataSource(dataSource);
ssfb.setTypeAliasesPackage("com.ffzs.consulauth.**.model");
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
ssfb.setMapperLocations(resolver.getResources("classpath*:**/generator/*.xml"));
return ssfb.getObject();
}
}
model,dao等文件通過idea上的generator插件生成即可。
重新寫一下獲取token時匹配用戶部分邏輯
UserDetailsServiceImpl.class
- 管理員給ROLE_ADMIN, ROLE_USER 權限
- 普通用戶只給 ROLE_USER 權限
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired(required = false)
private MyUserDao myUserDao;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
MyUser myUser = myUserDao.findByUsername(username);
System.out.println(username);
if (myUser == null) {
throw new UsernameNotFoundException("用戶不存在");
}
if (username.equals("admin") || username.equals("ffzs")){
return new User(username, myUser.getPassword(), AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_ADMIN, ROLE_USER"));
}
return new User(username, myUser.getPassword(), AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER"));
}
}
userdao要添加一個通過名字獲取信息的方法:
啓動類更改:
添加==@EnableDiscoveryClient==即可
運行&&測試
consul上註冊成功
使用postman發送post請求:
http://localhost:8731/oauth/token?client_id=consul_server&client_secret=123456&grant_type=password&username=ffzs&password=123zxc
成功獲取token
post請求中的參數及描述:
service配置
創建一個consul-service項目,用於提供登錄,註冊等服務:
1. 依賴:
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign:2.2.2.RELEASE'
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.1.2'
implementation 'org.springframework.cloud:spring-cloud-starter-consul-discovery:2.2.2.RELEASE'
implementation 'org.springframework.cloud:spring-cloud-starter-oauth2:2.2.2.RELEASE'
runtimeOnly 'mysql:mysql-connector-java'
2. 配置文件
bootstrap.yml:
spring:
application:
name: consul-service
cloud:
consul:
host: localhost
port: 8500
discovery:
prefer-ip-address: true
service-name: ${spring.application.name}
feign:
httpclient:
enabled: true
management:
endpoints:
web:
exposure:
include: "*"
endpoint:
health:
show-details: always
application.yml:
server:
port: 8777
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/ffzs?useUnicode=true&zeroDateTimeBehavior=convertToNull&autoReconnect=true&characterEncoding=utf-8
username: root
password: 123zxc
logging:
level:
root: INFO
org.springframework.web: INFO
org.springframework.security: INFO
org.springframework.security.oauth2: INFO
3. Mybatis配置
dao,model跟uaa的一樣
mybatis的配置文件跟uaa的也基本相同:
@Configuration
@MapperScan("com.ffzs.consulservice.**.dao")
public class MybatisConfig {
@Autowired
private DataSource dataSource;
@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
SqlSessionFactoryBean ssfb = new SqlSessionFactoryBean();
ssfb.setDataSource(dataSource);
ssfb.setTypeAliasesPackage("com.ffzs.consulservice.**.model");
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
ssfb.setMapperLocations(resolver.getResources("classpath*:**/generator/*.xml"));
return ssfb.getObject();
}
}
4. Resource配置
ResourceConfiguration.class:
- 配置了兩個測試路徑,user路徑用於登錄,不做權限限制
/hello/admin", "/hello/header
只右ADMIN權限才能訪問
@Configuration
@EnableResourceServer
public class ResourceConfiguration extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/actuator/**").permitAll()
.antMatchers("/user/**").permitAll()
.antMatchers("/hello/user", "hello/test").hasRole("USER")
.antMatchers("/hello/admin", "/hello/header").hasAnyRole("ADMIN", "USER")
.antMatchers("/**").authenticated();
}
/**
* tokenServices 定義Token Service 用ResourceServerTokenservices類,配置Token是如何編碼和解碼的 可以用RemoteTokenServices類,即Resource Server採用遠程授權服務器進行Token解碼,這時也不需要配置此選項
*
* @param resources
* @throws Exception
*/
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources
.resourceId("server") // 配置資源Id。
.tokenStore(jwtTokenStore());
}
@Bean
protected JwtAccessTokenConverter jwtTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("fanfanzhisu");
return converter;
}
@Bean
public TokenStore jwtTokenStore() {
return new JwtTokenStore(jwtTokenConverter());
}
}
5. 註冊功能
添加一個功能用於註冊,service
public int insertUser(String username, String password){
MyUser user=myUserDao.findByUsername(username);
if (user != null){
user.setPassword(encoder.encode(password));
return myUserDao.updateByPrimaryKeySelective(user);
}else{
MyUser myUser=new MyUser();
myUser.setUsername(username);
myUser.setPassword(encoder.encode(password));
return myUserDao.insertSelective(myUser);
}
}
controller:
@PostMapping("/register")
public String postUser(@RequestParam("username") String username , @RequestParam("password") String password){
int back = userServiceDetail.insertUser(username,password);
return back == 1?"註冊成功":"註冊失敗";
}
6. 登錄功能
編寫一個service通過feign向註冊中心的consul-auth,也就是上面uaa發送登錄請求並獲取token
@FeignClient(value = "consul-auth")
public interface AuthServiceClient {
@PostMapping(value = "/oauth/token")
MyToken getToken(@RequestHeader("Content-Type") String content, @RequestParam("client_id") String client_id, @RequestParam("client_secret") String client_secret, @RequestParam("grant_type") String type,
@RequestParam("username") String username, @RequestParam("password") String password);
}
調用getToken獲取token,之後裝入userLoginDTO中:
public UserLoginDTO login(String username, String password){
MyUser user=myUserDao.findByUsername(username);
if (null == user) {
throw new RuntimeException("error username");
}
if(!encoder.matches(password,user.getPassword())){
throw new RuntimeException("error password");
}
MyToken myToken =authServiceClient.getToken("application/json", "consul_server","123456","password", username, password);
if(myToken ==null){
throw new RuntimeException("error internal");
}
UserLoginDTO userLoginDTO=new UserLoginDTO();
userLoginDTO.setMyToken(myToken);
userLoginDTO.setUser(user);
return userLoginDTO;
}
用於登入的controller:
@PostMapping("/login")
public UserLoginDTO login(@RequestParam("username") String username , @RequestParam("password") String password){
return userServiceDetail.login(username,password);
}
7. 測試登錄
測試註冊功能,訪問http://localhost:8777/user/register?username=xiaozhang&password=123456,結果如下:
查看數據庫,xiaozhang的用戶信息已經存入數據庫:
測試登入功能,http://localhost:8777/user/login?username=xiaozhang&password=123456 ,返回token說明登錄成功:
8. 測試權限
因爲一些業務的需求會有一些端口需要鑑權,編寫用於測試的controller:
@RestController
@RequestMapping("hello")
public class TestController {
@GetMapping("user")
public String user(){
return "hello!!! 普通用戶 !!!";
}
@GetMapping("admin")
public String admin(){
return "hello!!! 權限dog !!!!";
}
@GetMapping("role")
public String test() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
StringBuilder res = new StringBuilder()
.append("用戶名: ").append(authentication.getName()).append("\n")
.append("權限情況: ");
for (Object it :authentication.getAuthorities().toArray()) {
res.append(it).append("\t");
}
return res.toString();
}
@RequestMapping("header")
public String header(HttpServletRequest request) {
StringBuilder html = new StringBuilder("<table border='2' cellspacing='0'>");
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String key = headerNames.nextElement();
html.append("<tr><td>").append(key).append("</td><td>").append(request.getHeader(key)).append("</td></tr>");
}
html.append("</table>");
return html.toString();
}
}
幾個端口權限設置如下:
.antMatchers("/hello/user", "hello/test").hasRole("USER")
.antMatchers("/hello/admin", "/hello/header").hasAnyRole("ADMIN", "USER")
- USER 權限可以訪問 “/hello/user”, “hello/test”
- ADMIN 都可以訪問
設置token,使用postman可以直接設置,如下圖,不使用postman的話可以寫道header裏:
這時我們訪問,http://localhost:8777/hello/user,可以訪問user;
訪問,http://localhost:8777/hello/admin,因爲沒有權限,限制訪問:
訪問,http://localhost:8777/hello/role,這裏我們可以看到xiaozhang只有ROLU_USER的權限:
更換用戶,使用ffzs
賬號登錄,更換獲得token,權限都token都長了:
使用ffzs的賬號訪問http://localhost:8777/hello/role
ROLE_ADMIN ROLE_USER同時擁有兩個權限:
這時我們試試用這個賬號能否訪問http://localhost:8777/hello/admin
有了ADMIN權限可以正常訪問了。
看一下header,訪問 http://localhost:8777/hello/header
可見token在header裏的形式,如果不容postman發送請求就添加通過將"authorization":"Bearer token"
添加到你的header中就可以訪問了