Spring boot整合Oauth2实现单点登陆

Spring boot整合Oauth2实现单点登陆(授权码模式)

本次使用spring boot 2.2.7开发,过程中遇到一些问题记录一下。

认证服务端

pom文件示例
<dependencies>
	    <!-- 使用redis存储token -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-redis</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<!-- oauth2 -->
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-oauth2</artifactId>
		</dependency>
		<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.12</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <!-- 自定义登陆界面  -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
</dependencies>

注意!!这里应用的是spring-cloud-starter-oauth2,所以pom需要指定springcloud的版本,并声明springcloud的dependencyManagement

<properties>
		<java.version>1.8</java.version>
    	<!-- 这里springboot版本为2.2.7,对应cloud版本是这个 -->
		<spring-cloud.version>Hoxton.SR4</spring-cloud.version>
</properties>

<dependencyManagement>
		<dependencies>
			<dependency>
				<groupId>org.springframework.cloud</groupId>
				<artifactId>spring-cloud-dependencies</artifactId>
				<version>${spring-cloud.version}</version>
				<type>pom</type>
				<scope>import</scope>
			</dependency>
		</dependencies>
</dependencyManagement>
系统配置文件application.yml
server:
 port: 8080
spring:
#redis配置信息
 redis:
  ..........
 #jpa配置信息
 jpa:
  ..........
 #数据库相关信息
 datasource: 
  .......... 
#模板引擎
 thymeleaf:
  ..........
#开启debug日志
logging:
 level:
  org.springframework: debug
实体类

​ 这里身份验证有两步,首先spring security验证用户名,密码,然后跳转到/oauth/authorize验证客户端的信息,之后授权再返回授权码。

​ 我们需要建两个实体类,用户的和客户端的。(这里的客户端指的是应用,比如文章后半段那个测试客户端)

用户实体UserInfo

​ 用户实体字段可以根据实际情况定义,只有id用户名密码也可以。这里的例子是电话号码telno作为用户名。

@Entity
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UserInfo{
    @Id
    private long id;
    
	private String password;

    private long telno;
    .........
}

客户端实体OauthClientDetails

​ 客户端实体字段要求会严格一些,字段名不一定要一样,但是一定要有,获取授权码时会用到,其他个性化字段也可以加。

@Data
@Entity
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class OauthClientDetails {

    @Id
    private String clientId;
    private String clientSecret;
    private String authorities;
    private String authorizedGrantTypes;
    private String resourceIds;
    private String webServerRedirectUri;
    private String scope;
    private int accessTokenValidity;
    private int refreshTokenValidity;
    @Column(length = 4096)
    private String additionalInformation;
    private String autoapprove;
}
实体对应repository
public interface ClientDetailsRepository 
    extends JpaRepository<OauthClientDetails, String> {	
}
public interface UserInfoRepository extends JpaRepository<UserInfo, Integer>{
    //根据telno查询用户信息
	public Optional<UserInfo> findUserInfoByTelno(Long telno);
}
新建user与client对应的service
用户信息UserDetailsServiceImpl

根据security的规则,需要继承UserDetailsService,实现loadUserByUsername方法:

@Service("testUserDetailService")//设置bean name,防止spring bean注入时冲突
public class UserDetailsServiceImpl implements UserDetailsService {
	
	private final UserInfoRepository userInfoRepository;
	@Autowired
	public UserDetailsServiceImpl(UserInfoRepository userInfoRepository) {
		this.userInfoRepository = userInfoRepository;
	}
	@Override
	public UserDetails loadUserByUsername(String arg0)
			throws UsernameNotFoundException {
		return userInfoRepository.findUserInfoByTelno(Long.parseLong(arg0))
				.map(userInfo -> {
					User.UserBuilder builder = User.builder();
					builder = builder.username(String.valueOf(userInfo.getTelno()));
					builder = builder.password(userInfo.getPassword());
					builder = builder.accountLocked(false);
                    //这里手动设置用户角色,可以通过数据库查询结果替换
					builder = builder.authorities(
                        Arrays.asList(new SimpleGrantedAuthority("ROLE_USER")));
					return builder.build();
				}).orElseThrow(() -> 
                               new UsernameNotFoundException(
                                   String.format("UserName not found.[%s]",arg0)));
	}

}
客户端信息ClientDetailsServiceimpl

这里也需要根据规则继承ClientDetailsService,实现loadClientByClientId

@Service("testClientDetailsService")//设置bean name
public class ClientDetailsServiceimpl implements ClientDetailsService {
	private final ClientDetailsRepository clientDetailsRepository;
	private final ObjectMapper objectMapper;
	public ClientDetailsServiceimpl(
        ClientDetailsRepository clientDetailsRepository,ObjectMapper objectMapper) {
		this.clientDetailsRepository = clientDetailsRepository;
		this.objectMapper = objectMapper;
	}	
	@Override
	public ClientDetails loadClientByClientId(String arg0)
			throws ClientRegistrationException {
		Optional<OauthClientDetails> findById = null ;
		try {
			findById = clientDetailsRepository.findById(arg0);
		} catch (Exception e2) {
			e2.printStackTrace();
		}
        //下面都是设置client的属性,仔细看看
		return findById
				.map(clientDetails -> {
					BaseClientDetails details = new BaseClientDetails();
					details.setClientId(clientDetails.getClientId());
					details.setClientSecret(clientDetails.getClientSecret());
					details.setAuthorizedGrantTypes(
                        Arrays.asList(
                            clientDetails.getAuthorizedGrantTypes().split(",")));
                    .................省略,具体查看github
                    //根据数据库信息自动授权,这样用户登陆可以跳过授权页面
                    if("true".equalsIgnoreCase(clientDetails.getAutoapprove()))
					details.setAutoApproveScopes(
                        Arrays.asList(
                            Optional.ofNullable(
                                clientDetails.getScope()).orElse("").split(",")));
                    
					try {
						if(clientDetails.getAdditionalInformation()!=null){
							details.setAdditionalInformation(
                                objectMapper.readValue(
                                    clientDetails.getAdditionalInformation(),
                                    HashMap.class));
						}
					} catch (Exception e) {
						e.printStackTrace();
					}
					return details;
				}).orElseThrow(BadClientCredentialsException::new);
	}
}
权限验证配置
将redisTokenStore放到spring容器中
@Configuration
public class RedisTokenStoreCOnfig {
	@Autowired
	private RedisConnectionFactory connect;
	@Bean
	public RedisTokenStore getRedisTokenStore(){
		return new RedisTokenStore(connect);
	}
}

配置spring security

测试过程中有出现获取token的路径受限的问题,需要开放权限/oauth/**

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
	@Autowired
	private PasswordEncoder passwordEncoder;
    //这个我是在启动类声明的bean:BCryptPasswordEncoder
	@Autowired
	@Qualifier("testUserDetailService")
	private UserDetailsService userDetailsService;
	
	@Bean("testAuthenticationManager")
	@Override
	public AuthenticationManager authenticationManagerBean() throws Exception {
		return super.authenticationManagerBean();
	}
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http
		.authorizeRequests()
		.antMatchers("/static/**","/oauth/**","/test","/auth/login").permitAll()
		.antMatchers(HttpMethod.OPTIONS, "/**").anonymous()
		.anyRequest().authenticated()
        //设置自定义登陆界面
		.and().formLogin().loginPage("/auth/login")
        //设置登陆界面表单提交路径,springSecurity登录方法拦截地址
        .loginProcessingUrl("/oauth/authorize")
		.permitAll()
		.and().httpBasic()//允许basic,不然进不到授权页面
		.and()
		.csrf().disable();
	}
    /**
	 * 配置spring security验证方式,用户名密码的验证是基于security的
	 * 所以要在这里设置,不然会报需要先通过spring security验证
	 */
	@Override
	protected void configure(AuthenticationManagerBuilder auth)
			throws Exception {
		auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);
	}
}

对于formLogin需要注意,loginPage是指定登陆界面,loginProcessingUrl是指定登陆界面表单提交路径action='',设置需要spring security过滤的url,当请求到后台时会进入security对应的过滤器,实现它的登录逻辑,我看网上其他文章很多都说些/login或者/authentication/form,是否正确有待检验,根据默认实现的登陆逻辑看,点击登陆之后跳转的路径就是/oauth/authorize

分析一下:oauth2在获取授权码时访问的路径是http://localhost:8080/oauth/authorize?response_type=code&client_id=test&redirect_uri=http://localhost:8080/test。就是说我们不管是手动请求token还是客户端的sso请求token都需要通过这个url访问授权服务器,现在我们自定义登陆界面之后会跳转到登陆界面的url,如果我们表单提交路径不是这个的话spring security的验证也能进行,但是不会被oauth2的过滤器捕获,结果就是spring security验证通过,跳转到默认的主页/。要解决这个问题就需要自定义登陆成功之后的逻辑successHandler,成本比较高。

配置oauth2

这个内容网上关于oauth2的文章大同小异,具体有哪些是必须的,还需要再研究一下,这里暂且这样吧。

@Configuration
@EnableAuthorizationServer
@Slf4j
public class Oauth2COnfig extends AuthorizationServerConfigurerAdapter {
	@Autowired
	@Qualifier("testUserDetailService")//由bean name注入
	private UserDetailsService userDetailsService;
	@Autowired
	@Qualifier("testClientDetailsService")
	private ClientDetailsService clientDetailsService;
	@Autowired
	private PasswordEncoder passwordEncoder;
	@Autowired
	@Qualifier("testAuthenticationManager")
	private AuthenticationManager authenticationManager;
	@Autowired
	private RedisTokenStore redisTokenStore;
	@Override
	public void configure(AuthorizationServerEndpointsConfigurer endpoints)
			throws Exception {
		endpoints.tokenStore(redisTokenStore)
				.tokenServices(getTokenService())
				.authenticationManager(authenticationManager)
				.userDetailsService(userDetailsService);
		if(log.isDebugEnabled()){
			log.debug("AuthorizationServerEndpointsConfigurer configuration finished");
		}
	}
	@Override
	public void configure(AuthorizationServerSecurityConfigurer security)
			throws Exception {
		security.tokenKeyAccess("permitAll()")
				.checkTokenAccess("isAuthenticated()")
				.passwordEncoder(passwordEncoder);		
		if(log.isDebugEnabled()){
			log.debug("AuthorizationServerSecurityConfigurer configuration finished");
		}				
	}
	@Override
	public void configure(ClientDetailsServiceConfigurer clients)
			throws Exception {
		clients.withClientDetails(clientDetailsService);		
		if(log.isDebugEnabled()){
			log.debug("ClientDetailsServiceConfigurer configuration finished");
		}
	}
	@Bean
	public DefaultTokenServices getTokenService(){
		DefaultTokenServices service = new DefaultTokenServices();
		service.setTokenStore(redisTokenStore);
		service.setClientDetailsService(clientDetailsService);
		service.setSupportRefreshToken(true);
		return service;
	}
}
配置resourceServer

注意,测试时发现没设置resourceid会出现请求在login与authentication之间反复横跳,导致重定向过多。还有个大问题要注意,服务端测试时回调地址最好不要写/login,因为oauth2会拦截这个请求并转到默认主页,没有的话会一直报404,配置了permitall也会这样的。

@Configuration
@EnableResourceServer//声明为一个资源服务器
public class OauthResourceServerConfig extends ResourceServerConfigurerAdapter {
	@Override
	public void configure(HttpSecurity http) throws Exception {
        //这里的配置跟security里面的是一个意思,但是我测试几遍发现访问的时候是用的这里的规则
        //所以没研究透彻之前保持两边一直,但是formlogin那个经过测试,这里是不用写的,写了也没用
		http
		.authorizeRequests()
		.antMatchers("/oauth/**").permitAll()
		.antMatchers("/test").permitAll()//测试的回调路径,不放开会导致获取授权码时错误
		.antMatchers("/auth/login").permitAll()
		.antMatchers(HttpMethod.OPTIONS, "/**").anonymous()
		.anyRequest().authenticated()
		.and().httpBasic()//开启后才能进入授权页面
		.and()
		.csrf().disable();
	}
    @Override
	public void configure(ResourceServerSecurityConfigurer resources)
			throws Exception {
		resources.resourceId("testresourceid");
	}
}

配置结束。

template文件夹下新建login.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>统一验证登陆界面</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
</head>
<body>
	<div class="container" style="margin-top:250px">
		<div class="row clearfix">
			<div class="col-md-4 column">
			</div>
			<div class="col-md-4 column">
				<form class="form-horizontal" action="/oauth/authorize" method="post" role="form">
					<div class="form-group">
						 <label for="username" class="col-sm-2 control-label">账号</label>
						<div class="col-sm-8">
							<input type="text" class="form-control" id="username" name="username" />
						</div>
					</div>
					<div class="form-group">
						 <label for="password" class="col-sm-2 control-label">密码</label>
						<div class="col-sm-8">
							<input type="password" class="form-control" id="password" name = "password" />
						</div>
					</div>
					<div class="form-group">
						<div class="col-sm-offset-2 col-sm-10">
							<div class="checkbox">
								 <label><input type="checkbox" />记住我</label>
							</div>
						</div>
					</div>
					<div class="form-group">
						<div class=" col-sm-10">
							 <button type="submit" class="btn btn-primary btn-lg btn-block">登陆</button>
						</div>
					</div>
				</form>
			</div>
			<div class="col-md-4 column">
			</div>
		</div>
	</div>
</body>
<!-- 最新的 Bootstrap 核心 JavaScript 文件 -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>

</html>
controller示例
	@GetMapping("/auth/login")
	public ModelAndView formlogin(ModelAndView mv){
		mv.setViewName("/login.html");
		return mv;
	}
postman测试
数据库 客户端信息示例

在这里插入图片描述

  • 浏览器访问http://localhost:8080/oauth/authorize?response_type=code&client_id=test&redirect_uri=http://localhost:8080/test,需要注意的是,client_id与redirect_uri必须对应数据库内的数据,uri不存在会不允许跳转

  • 成功跳转之后会在地址栏出现code的参数。使用postman获取token,url为/oauth/token

    • 设置请求头身份信息,client_secret可以为空,根据数据库内容定义,headers里面添加content-type为application/x-www-form-urlencoded。

    在这里插入图片描述

    • 设置参数

      在这里插入图片描述

  • 结果示例

    {
        "access_token": *******************,
        "token_type": "bearer",
        "refresh_token": "*******************",
        "expires_in": 7199,
        "scope": "read write"
    }
    

认证服务器的内容就这些了。下面是客户端的内容。

客户端示例

客户端只需要配置security,再加上@EnableOAuth2Sso,这样会加入sso相关的filter,实现获取token的过程。

pom文件示例
	<dependency>
		<groupId>org.springframework.cloud</groupId>
		<artifactId>spring-cloud-starter-oauth2</artifactId>
	</dependency>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-web</artifactId>
	</dependency>
配置文件示例

注意!!server.servlet.session.cookie.name必须设置,不然会导致登陆之后跳转不到我们需要请求的路径。具体原因百度一下。

#认证服务器地址
auth-server: http://*****:8080
server:
 port: 9090
 servlet:
  session:
   cookie:
    name: sso-client
security:
 basic: 
  enable: false
  #sso配置,对应服务端数据库的client的一条信息
 oauth2:
  client:
   clientId: test
   clientSecret: test-8080
   accessTokenUri: ${auth-server}/oauth/token
   userAuthorizationUri: ${auth-server}/oauth/authorize
   scope: read,write
   auto-approve-scopes: '.*'
  resource:
   userInfoUri: ${auth-server}/user
logging:
 level:
  org.springframework: debug
客户端security配置

​ 注意把/oauth路径放开,不然可能会导致访问不到。

​ 加入@EnableOAuth2Sso,这个注解只能加在WebSecurityConfigurerAdapter实现类上,因为它有自己默认的WebSecurityConfigurerAdapter实现,当它标注的类不是继承的WebSecurityConfigurerAdapter,那么它会加载默认的配置类,会导致冲突。

@Configuration
@EnableOAuth2Sso
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http
		.authorizeRequests().antMatchers("/oauth/**").permitAll()
		.........
		.and()
		.csrf().disable();
	}
}

​ 基本的单点登录功能就实现了,访问客户端受限url时会先去认证服务器获取token,以同样的方式新建多个客户端,对应服务端的多条client信息,可以免登录。这里实现的客户端对应的就是认证服务器的client。有兴趣的话可以把服务端的formlogin先去掉,debug看看。

认证服务器github地址:https://github.com/superDSB/ssoServerDemo

当它标注的类不是继承的WebSecurityConfigurerAdapter,那么它会加载默认的配置类,会导致冲突。

@Configuration
@EnableOAuth2Sso
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http
		.authorizeRequests().antMatchers("/oauth/**").permitAll()
		.........
		.and()
		.csrf().disable();
	}
}

​ 基本的单点登录功能就实现了,访问客户端受限url时会先去认证服务器获取token,以同样的方式新建多个客户端,对应服务端的多条client信息,可以免登录。这里实现的客户端对应的就是认证服务器的client。有兴趣的话可以把服务端的formlogin先去掉,debug看看。

认证服务器github地址:https://github.com/superDSB/ssoServerDemo

客户端示例github地址:https://github.com/superDSB/ssoclientdemo

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值