单点登录
什么是单点登录?单点登录全称Single Sign On(以下简称SSO),是指在多系统应用群中登录一个系统,便可在其他所有系统中得到授权而无需再次登录,包括单点登录与单点注销两部分
相比于单系统登录,sso需要一个独立的认证中心,只有认证中心能接受用户的用户名密码等安全信息,其他系统不提供登录入口,只接受认证中心的间接授权。间接授权通过令牌实现,sso认证中心验证用户的用户名密码没问题,创建授权令牌,在接下来的跳转过程中,授权令牌作为参数发送给各个子系统,子系统拿到令牌,即得到了授权,可以借此创建局部会话,局部会话登录方式与单系统的登录方式相同。这个过程,也就是单点登录的原理。
oauth2
在oauth中我们通常将他分为认证中心和资源中心。二者可以放在一起,但是对于微服务来说,每个独自的微服务可能就是一个资源中心。
在oauth2中有几种授权模式:
1、授权码模式
2、密码模式
3、客户端模式
4、简易模式
具体请求流程不是本文中点,我们着重讲解Spring cloud oauth2搭建认证中心。
在oauth中我们通常将他分为认证中心和资源中心。
对于我们的微服务来说,每个独立的微服务即是一个个资源中心,用户想要请求微服务的数据,需要携带认证中心颁发的tocken。
1.引入相关pom依赖
这里我们使用1.5.2.RELEASE的spring boot版本,因为我们会将oauth2交给cloud管理,所以我们同时引入了cloud的依赖,cloud版本使用SR5
org.springframework.boot
spring-boot-starter-parent
1.5.2.RELEASE
org.springframework.cloud
spring-cloud-dependencies
Finchley.SR5
pom
import
org.springframework.cloud
spring-cloud-starter-eureka-server
org.springframework.boot
spring-boot-starter-test
test
net.logstash.logback
logstash-logback-encoder
org.springframework.cloud
spring-cloud-starter-security
true
org.springframework.cloud
spring-cloud-starter-oauth2
org.springframework.security.oauth
spring-security-oauth2
javax.inject
javax.inject
org.springframework.boot
spring-boot-starter-data-redis
mysql
mysql-connector-java
runtime
org.mybatis.spring.boot
mybatis-spring-boot-starter
com.alibaba
fastjson
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.声明一个授权服务器
声明一个授权服务器只需要继承 AuthorizationServerConfigurerAdapter,添加
@EnableAuthorizationServer 注解。
@EnableAuthorizationServer 这个注解告诉 Spring 这个应用是 OAuth2 的认证中心。
并且复写如下三个方法:
ClientDetailsServiceConfigurer:这个configurer定义了客户端细节服务。客户详细信息可以被初始化。
AuthorizationServerSecurityConfigurer:在令牌端点上定义了安全约束。
AuthorizationServerEndpointsConfigurer:定义了授权和令牌端点和令牌服务。
配置客户端详细步骤
ClientDetailsServiceConfigurer 类(AuthorizationServerConfigurer类中的一个调用类)可以用来定义一个基于内存的或者JDBC的客户端信息服务。
客户端对象重要的属性有:
clientId:(必须)客户端id。
secret:(对于可信任的客户端是必须的)客户端的私密信息。
scope:客户端的作用域。如果scope未定义或者为空(默认值),则客户端作用域不受限制。
authorizedGrantTypes:授权给客户端使用的权限类型。默认值为空。
authorities:授权给客户端的权限(Spring普通的安全权限)。
在运行的应用中,可以通过直接访问隐藏的存储文件(如:JdbcClientDetailsService中用到的数据库表)或者通过实现ClientDetailsManager 接口(也可以实现ClientDetailsService 接口,或者实现两个接口)来更新客户端信息。
@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: 用户验证信息的保存策略,可以存储在内存中,关系型数据库中,redis中
* @param
* @return TokenStore
* @throws
*/
@Bean
public TokenStore tokenStore(){
//return new RedisTokenStore(connectionFactory);
//return new InMemoryTokenStore();
return new JdbcTokenStore(dataSource);
}
@Bean // 声明 ClientDetails实现
public ClientDetailsService clientDetails() {
return new JdbcClientDetailsService(dataSource);
}
/**
*
* 这个方法主要是用于校验注册的第三方客户端的信息,可以存储在数据库中,默认方式是存储在内存中,如下所示,注释掉的代码即为内存中存储的方式
*/
@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 关于这个配置项,是在 OAuth2协议中,认证成功后的回调地址,此值同样可以配置多个*/
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参数 可以考虑使用[DefaultTokenServices],它使用随机值创建令牌
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);
}
/**
允许表单验证,浏览器直接发送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:
* 请求授权:
* spring security 使用以下匹配器来匹配请求路劲:
* antMatchers:使用ant风格的路劲匹配
* regexMatchers:使用正则表达式匹配路劲
* anyRequest:匹配所有请求路劲
* 在匹配了请求路劲后,需要针对当前用户的信息对请求路劲进行安全处理。
* 2:定制登录行为。
* formLogin()方法定制登录操作
* loginPage()方法定制登录页面访问地址
* defaultSuccessUrl()登录成功后转向的页面
* 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自定义用户认证实现
认证是由AuthenticationManager 来管理的,
但是真正进行认证的是 AuthenticationManager 中定义的AuthenticationProvider。
AuthenticationManager 中可以定义有多个 AuthenticationProvider,
我们在四步骤中AuthenticationManagerBuilder中添加了当前的Provider 。
@Named
@Component
public class MyAuthenticationProvider extends DaoAuthenticationProvider {
/** 规则校验 */
@Resource(name = "passwordService")
private PasswordService passwordService;
protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
// 构造函数中注入
@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);
//按登录规则校验用户
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
自定义需要实现UserDetailsService接口,并且重写loadUserByUsername方法。返回的用户信息需要实现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 paramMap=new HashMap();
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方法,将用户的角色作为权限
*/
@Override
public Collection extends GrantedAuthority> getAuthorities() {
//TODO 后续带完善
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相关数据库表结构
为了文章不过于冗余,表结构和代码将在将在gitlab上共享
https://gitlab.com/liguobao666/cloud-service
8相关配置文件
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:
这样认证服务器就搭建完成了,浏览器只需要携带tocken就可以访问资源服务器上的相关信息。