單點登錄
什么是單點登錄孙蒙?單點登錄全稱Single Sign On(以下簡稱SSO),是指在多系統(tǒng)應(yīng)用群中登錄一個系統(tǒng)悲雳,便可在其他所有系統(tǒng)中得到授權(quán)而無需再次登錄挎峦,包括單點登錄與單點注銷兩部分
相比于單系統(tǒng)登錄,sso需要一個獨立的認證中心合瓢,只有認證中心能接受用戶的用戶名密碼等安全信息坦胶,其他系統(tǒng)不提供登錄入口,只接受認證中心的間接授權(quán)歪玲。間接授權(quán)通過令牌實現(xiàn)迁央,sso認證中心驗證用戶的用戶名密碼沒問題掷匠,創(chuàng)建授權(quán)令牌滥崩,在接下來的跳轉(zhuǎn)過程中,授權(quán)令牌作為參數(shù)發(fā)送給各個子系統(tǒng)讹语,子系統(tǒng)拿到令牌钙皮,即得到了授權(quán),可以借此創(chuàng)建局部會話顽决,局部會話登錄方式與單系統(tǒng)的登錄方式相同短条。這個過程,也就是單點登錄的原理才菠。
oauth2
在oauth中我們通常將他分為認證中心和資源中心茸时。二者可以放在一起,但是對于微服務(wù)來說赋访,每個獨自的微服務(wù)可能就是一個資源中心可都。
在oauth2中有幾種授權(quán)模式:
1缓待、授權(quán)碼模式
2、密碼模式
3渠牲、客戶端模式
4旋炒、簡易模式
具體請求流程不是本文中點,我們著重講解Spring cloud oauth2搭建認證中心签杈。
在oauth中我們通常將他分為認證中心和資源中心瘫镇。
對于我們的微服務(wù)來說,每個獨立的微服務(wù)即是一個個資源中心答姥,用戶想要請求微服務(wù)的數(shù)據(jù)铣除,需要攜帶認證中心頒發(fā)的tocken。
1.引入相關(guān)pom依賴
這里我們使用1.5.2.RELEASE的spring boot版本鹦付,因為我們會將oauth2交給cloud管理通孽,所以我們同時引入了cloud的依賴,cloud版本使用SR5
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.2.RELEASE</version>
<relativePath></relativePath>
</parent>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.SR5</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- 熱部署 -->
<!-- <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency> -->
<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
</dependency>
<dependency>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
2.編寫spring boot啟動類
@SpringBootApplication
@Configuration
@EnableTransactionManagement
@MapperScan(basePackages = {
"com.dahaonetwork.smartfactory.authserver.mapper"
})
public class AuthenticationApplication {
/** 主類 */
public static void main(String[] args) {
SpringApplication.run(AuthenticationApplication.class, args);
}
}
3.聲明一個授權(quán)服務(wù)器
聲明一個授權(quán)服務(wù)器只需要繼承 AuthorizationServerConfigurerAdapter睁壁,添加
@EnableAuthorizationServer 注解背苦。
@EnableAuthorizationServer 這個注解告訴 Spring 這個應(yīng)用是 OAuth2 的認證中心。
并且復(fù)寫如下三個方法:
ClientDetailsServiceConfigurer:這個configurer定義了客戶端細節(jié)服務(wù)潘明⌒屑粒客戶詳細信息可以被初始化。
AuthorizationServerSecurityConfigurer:在令牌端點上定義了安全約束钳降。
AuthorizationServerEndpointsConfigurer:定義了授權(quán)和令牌端點和令牌服務(wù)厚宰。
配置客戶端詳細步驟
ClientDetailsServiceConfigurer 類(AuthorizationServerConfigurer類中的一個調(diào)用類)可以用來定義一個基于內(nèi)存的或者JDBC的客戶端信息服務(wù)。
客戶端對象重要的屬性有:
clientId:(必須)客戶端id遂填。
secret:(對于可信任的客戶端是必須的)客戶端的私密信息铲觉。
scope:客戶端的作用域。如果scope未定義或者為空(默認值)吓坚,則客戶端作用域不受限制撵幽。
authorizedGrantTypes:授權(quán)給客戶端使用的權(quán)限類型。默認值為空礁击。
authorities:授權(quán)給客戶端的權(quán)限(Spring普通的安全權(quán)限)盐杂。
在運行的應(yīng)用中,可以通過直接訪問隱藏的存儲文件(如:JdbcClientDetailsService中用到的數(shù)據(jù)庫表)或者通過實現(xiàn)ClientDetailsManager 接口(也可以實現(xiàn)ClientDetailsService 接口哆窿,或者實現(xiàn)兩個接口)來更新客戶端信息链烈。
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception{...}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {...}
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {...}
具體代碼如下所示:這里我們將tocken信息存儲在mysql中,分布式下你可以存儲在redis中挚躯,全局共享强衡。
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter{
@Autowired
@Qualifier("authenticationManagerBean")
private AuthenticationManager authenticationManager;
@Autowired
private RedisConnectionFactory connectionFactory;
@Autowired
private DataSource dataSource;
/**
* @Title: tokenStore
* @Description: 用戶驗證信息的保存策略,可以存儲在內(nèi)存中码荔,關(guān)系型數(shù)據(jù)庫中漩勤,redis中
* @param
* @return TokenStore
* @throws
*/
@Bean
public TokenStore tokenStore(){
//return new RedisTokenStore(connectionFactory);
//return new InMemoryTokenStore();
return new JdbcTokenStore(dataSource);
}
@Bean // 聲明 ClientDetails實現(xiàn)
public ClientDetailsService clientDetails() {
return new JdbcClientDetailsService(dataSource);
}
/**
*
* 這個方法主要是用于校驗注冊的第三方客戶端的信息号涯,可以存儲在數(shù)據(jù)庫中,默認方式是存儲在內(nèi)存中锯七,如下所示链快,注釋掉的代碼即為內(nèi)存中存儲的方式
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception{
// clients.inMemory()
// .withClient("client").secret("123456").scopes("read")
// .authorizedGrantTypes("authorization_code", "password", "refresh_token")
// .authorities("ROLE_CLIENT", "ROLE_TRUSTED_CLIENT");
// //.authorizedGrantTypes("password","authorization_code","client_credentials","refresh_token");
// //.authorizedGrantTypes("password","refresh_token");
// //.redirectUris("https://www.getpostman.com/oauth2/callback");
// /*redirectUris 關(guān)于這個配置項,是在 OAuth2協(xié)議中眉尸,認證成功后的回調(diào)地址域蜗,此值同樣可以配置多個*/
clients.withClientDetails(clientDetails());
clients.jdbc(dataSource);
}
/**
* 這個方法主要的作用用于控制token的端點等信息
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager);
endpoints.tokenStore(tokenStore());
//endpoints.userDetailsService(userService);
// 配置TokenServices參數(shù) 可以考慮使用[DefaultTokenServices],它使用隨機值創(chuàng)建令牌
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setTokenStore(endpoints.getTokenStore());
tokenServices.setSupportRefreshToken(true);
tokenServices.setClientDetailsService(endpoints.getClientDetailsService());
tokenServices.setTokenEnhancer(endpoints.getTokenEnhancer());
tokenServices.setAccessTokenValiditySeconds( (int) TimeUnit.DAYS.toSeconds(30)); // 30天
endpoints.tokenServices(tokenServices);
}
/**
允許表單驗證噪猾,瀏覽器直接發(fā)送post請求即可獲取tocken
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
oauthServer.tokenKeyAccess("permitAll()").checkTokenAccess(
"isAuthenticated()");
oauthServer.allowFormAuthenticationForClients();
}
}
4開啟Spring Security的功能
spring security用來驗證用戶賬號密碼霉祸,對請求路勁做處理等。繼承WebSecurityConfigurerAdapter 使用@EnableWebMvcSecurity 注解開啟Spring Security的功能袱蜡。
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter{
@Autowired
private MyAuthenticationProvider provider;
@Autowired
private UserDetailsService userService;
/**
* 用戶認證
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(provider);
auth.userDetailsService(userService);
// auth.inMemoryAuthentication()
// .withUser("user").password("123456").authorities("ROLE_USER");
}
/**
* 1:
* 請求授權(quán):
* spring security 使用以下匹配器來匹配請求路勁:
* antMatchers:使用ant風格的路勁匹配
* regexMatchers:使用正則表達式匹配路勁
* anyRequest:匹配所有請求路勁
* 在匹配了請求路勁后丝蹭,需要針對當前用戶的信息對請求路勁進行安全處理。
* 2:定制登錄行為坪蚁。
* formLogin()方法定制登錄操作
* loginPage()方法定制登錄頁面訪問地址
* defaultSuccessUrl()登錄成功后轉(zhuǎn)向的頁面
* permitAll()
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers(
StaticParams.PATHREGX.API,
StaticParams.PATHREGX.CSS,
StaticParams.PATHREGX.JS,
StaticParams.PATHREGX.IMG).permitAll()//允許用戶任意訪問
.anyRequest().authenticated()//其余所有請求都需要認證后才可訪問
.and()
.formLogin()
//.loginPage("/login/login.do") /
//.defaultSuccessUrl("/hello2")
.permitAll();//允許用戶任意訪問
http.csrf().disable();
}
/**
* 密碼模式下必須注入的bean authenticationManagerBean
* 認證是由 AuthenticationManager 來管理的奔穿,
* 但是真正進行認證的是 AuthenticationManager 中定義的AuthenticationProvider。
* AuthenticationManager 中可以定義有多個 AuthenticationProvider
*/
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
5自定義用戶認證實現(xiàn)
認證是由AuthenticationManager 來管理的敏晤,
但是真正進行認證的是 AuthenticationManager 中定義的AuthenticationProvider贱田。
AuthenticationManager 中可以定義有多個 AuthenticationProvider,
我們在四步驟中AuthenticationManagerBuilder中添加了當前的Provider 嘴脾。
@Named
@Component
public class MyAuthenticationProvider extends DaoAuthenticationProvider {
/** 規(guī)則校驗 */
@Resource(name = "passwordService")
private PasswordService passwordService;
protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
// 構(gòu)造函數(shù)中注入
@Inject
public MyAuthenticationProvider(UserDetailsService userDetailsService)
{
this.setUserDetailsService(userDetailsService);
}
/**
* 自定義驗證方式
*/
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = authentication.getName();
String password = (String) authentication.getCredentials();
MyUserDetails userDetails = (MyUserDetails)
this.getUserDetailsService().loadUserByUsername(username);
//按登錄規(guī)則校驗用戶
passwordService.validateRules(userDetails.getUser(), password);
Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities();
Authentication authenticationToken = new UsernamePasswordAuthenticationToken(JSON.toJSONString(userDetails,SerializerFeature.WriteMapNullValue), password, authorities);
return authenticationToken;
}
@Override
public boolean supports(Class<?> arg0) {
return true;
}
}
6自定義UserDetailsService
自定義需要實現(xiàn)UserDetailsService接口男摧,并且重寫loadUserByUsername方法。返回的用戶信息需要實現(xiàn)UserDatails接口译打。
@Service("userInfo")
public class UserInfoService implements UserDetailsService{
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// TODO Auto-generated method stub
Map<String, String> paramMap=new HashMap<String, String>();
paramMap.put("loginId", username);
User user=userMapper.getUserByloginIds(paramMap);
if(user ==null) {
throw new BadCredentialsException(Constants.getReturnStr(Constants.USER_NOT_FOUND, Constants.USER_NOT_FOUND_TIPS));
}
MyUserDetails userDetails = new MyUserDetails();
userDetails.setUserName(username);
userDetails.setPassword(user.getPassword());
userDetails.setUser(user);
return userDetails;
}
}
public class MyUserDetails implements UserDetails{
private static final long seriaVersionUID=1L;
private String userName;
private String password;
private User user;
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public void setPassword(String password) {
this.password = password;
}
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
/**
* 重寫getAuthorities方法耗拓,將用戶的角色作為權(quán)限
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
//TODO 后續(xù)帶完善
return AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_SUPER");
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return userName;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
7相關(guān)數(shù)據(jù)庫表結(jié)構(gòu)
為了文章不過于冗余,表結(jié)構(gòu)和代碼將在將在gitlab上共享
https://gitlab.com/liguobao666/cloud-service
8相關(guān)配置文件
spring.application.name=oauth-server
server.port=8043
server.context-path=/uaa
logging.level.org.springframework.security=DEBUG
security.oauth2.resource.serviceId= ${PREFIX:}resource
security.oauth2.resource.filter-order=3
database.url=jdbc:mysql://192.168.7.175:3306/oauth2
spring.datasource.url=${database.url}?useUnicode\=true&characterEncoding\=UTF-8&useOldAliasMetadataBehavior\=true
spring.datasource.username=devdb
spring.datasource.password=
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
logging.level.root=info
logging.level.com.dahaonetwork.smartfactory.authserver=debug
logging.level.org.springframework.security=info
#mybatis show sql
logging.level.org.springframework=WARN
spring.thymeleaf.cache=false
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
mybatis.mapper-locations=classpath:mapper/*.xml
9測試
這里我們使用密碼模式來獲取tocken:
這樣認證服務(wù)器就搭建完成了奏司,瀏覽器只需要攜帶tocken就可以訪問資源服務(wù)器上的相關(guān)信息乔询。