一、最简单认证服务器

1. pom依赖

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.1.0.RELEASE</version>
</dependency>

2. 配置application.yml

security:
oauth2:
client:
client-id: clientId
client-secret: clientSecret
scope: scope1, scope2, scope3, scope4
registered-redirect-uri: http://www.baidu.com
spring:
security:
user:
name: admin
password: admin

3. 开启@EnableAuthorizationServer,同时开启SpringSecurity用户登录认证

@SpringBootApplication
@EnableAuthorizationServer
public class SpringBootTestApplication { public static void main(String[] args) {
SpringApplication.run(SpringBootTestApplication.class, args);
} @Bean
public WebSecurityConfigurerAdapter webSecurityConfigurerAdapter() {
return new WebSecurityConfigurerAdapter() {
@Override
public void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity.formLogin().and().csrf().disable();
       }
     };
   }
}

4. 测试

(1)密码模式和客户端模式直接通过单元测试就可以完成

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class SpringBootTestApplicationTest { @Autowired
private TestRestTemplate restTemplate; @Test
public void token_password() {
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("grant_type", "password");
params.add("username", "admin");
params.add("password", "admin");
params.add("scope", "scope1 scope2");
String response = restTemplate.withBasicAuth("clientId", "clientSecret").
postForObject("/oauth/token", params, String.class);
System.out.println(response);
} @Test
public void token_client() {
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("grant_type", "client_credentials");
String response = restTemplate.withBasicAuth("clientId", "clientSecret").
postForObject("/oauth/token", params, String.class);
System.out.println(response);
} }

(2)授权码验证模式

  • 访问 http://127.0.0.1:8080/oauth/authorize?client_id=clientId&response_type=code,跳转到SpringSecurity默认的登录页面:
  • 输入用户名/密码:admin/admin,点击登录后跳转到确认授权页面:

  • 至少选中一个,然后点击Authorize按钮,跳转到 https://www.baidu.com/?code=tg0GDq,这样我们就拿到了授权码。

  • 通过授权码申请token:

    @Test
    public void token_code() {
    MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
    params.add("grant_type", "authorization_code");
    params.add("code", "tg0GDq");
    String response = restTemplate.withBasicAuth("clientId", "clientSecret").postForObject("/oauth/token", params, String.class);
    System.out.println(response);
    }

(3)刷新token

@Test
public void token_refresh() {
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("grant_type", "refresh_token");
params.add("refresh_token", "fb00358a-44e2-4679-9129-1b96f52d8d5d");
String response = restTemplate.withBasicAuth("clientId", "clientSecret").
postForObject("/oauth/token", params, String.class);
System.out.println(response);
}

刷新token功能报错,// todo 2018-11-08 此处留坑

二、比较复杂的认证服务器

上面我们搭建的认证服务器存在以下弊端:

  1. clientId和clientSecret是写死在配置文件里的。
  2. 用户信息写死在配置文件里。
  3. 通过clientId和clientSecret获取的code和token都存在内存中。第一:如果服务器宕机code和token会丢失;第二:不支持多点部署。

针对以上问题,我们要做的就是

  1. 将clientId和clientSecret等信息存储在数据库中。
  2. 将用户信息存储在数据库中。
  3. 将code和token存储在redis中。

接下来我们一步一步实现:

1. 创建测试用表及数据

drop table if exists test.oauth2_client;
create table test.oauth2_client (
id int auto_increment primary key,
clientId varchar(50),
clientSecret varchar(50),
redirectUrl varchar(2000),
grantType varchar(100),
scope varchar(100)
); insert into test.oauth2_client(clientId, clientSecret, redirectUrl, grantType, scope)
values ('clientId','clientSecret','http://www.baidu.com,http://www.csdn.net', 'authorization_code,client_credentials,password,implicit', 'scope1,scope2'); drop table if exists test.oauth2_user;
create table test.oauth2_user (
id int auto_increment primary key,
username varchar(50),
password varchar(50)
); insert into test.oauth2_user (username, password)
values ('admin','admin'); insert into test.oauth2_user (username, password)
values ('guest','guest');

创建测试用表及数据

  • 表oauth2_client:存储clientId、clientSecret及其他信息。本例只创建了一个client。
  • 表oauth2_user:用户信息。本例创建了两个用户:admin/admin、guest/guest。

2. Dao和Service

Dao和Service就不用废话了,肯定要有的

public class Oauth2Client {

    private int id;
private String clientId;
private String clientSecret;
private String redirectUrl;
private String grantType;
private String scope; public int getId() {
return id;
} public void setId(int id) {
this.id = id;
} public String getClientId() {
return clientId;
} public void setClientId(String clientId) {
this.clientId = clientId;
} public String getClientSecret() {
return clientSecret;
} public void setClientSecret(String clientSecret) {
this.clientSecret = clientSecret;
} public String getRedirectUrl() {
return redirectUrl;
} public void setRedirectUrl(String redirectUrl) {
this.redirectUrl = redirectUrl;
} public String getGrantType() {
return grantType;
} public void setGrantType(String grantType) {
this.grantType = grantType;
} public String getScope() {
return scope;
} public void setScope(String scope) {
this.scope = scope;
}
}

Oauth2Client

public class Oauth2User {

    private int id;
private String username;
private String password; public int getId() {
return id;
} public void setId(int id) {
this.id = id;
} public String getUsername() {
return username;
} public void setUsername(String username) {
this.username = username;
} public String getPassword() {
return password;
} public void setPassword(String password) {
this.password = password;
}
}

Oauth2User

@Repository
public class Oauth2Dao { private final JdbcTemplate jdbcTemplate; @Autowired
public Oauth2Dao(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
} public List<Oauth2Client> getOauth2ClientByClientId(String clientId) {
String sql = "select * from oauth2_client where clientId = ?";
return jdbcTemplate.query(sql, new String[]{clientId}, new BeanPropertyRowMapper<>(Oauth2Client.class));
} public List<Oauth2User> getOauth2UserByUsername(String username) {
String sql = "select * from oauth2_user where username = ?";
return jdbcTemplate.query(sql, new String[]{username}, new BeanPropertyRowMapper<>(Oauth2User.class));
} }

Oauth2Dao

@Service
public class Oauth2Service { private final Oauth2Dao oauth2Dao; @Autowired
public Oauth2Service(Oauth2Dao oauth2Dao) {
this.oauth2Dao = oauth2Dao;
} public List<Oauth2Client> getOauth2ClientByClientId(String clientId) {
return oauth2Dao.getOauth2ClientByClientId(clientId);
} public List<Oauth2User> getOauth2UserByUsername(String username) {
return oauth2Dao.getOauth2UserByUsername(username);
}
}

Oauth2Service

3. 增加pom依赖

因为要使用到数据库以及redis,所以我们需要增加如下依赖:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</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>
</dependency>

4. 修改启动主类,增加bean注册

(1)注册一个PasswordEncoder用于密码加密:

这样做的目的是:在我们的应用中,可能都多个地方需要我们对用户的明文密码进行加密。在这里我们统一注册一个PasswordEncoder,以保证加密算法的一致性。

@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}

(2)注册一个UserDetailsService用于用户身份认证

@Bean
public UserDetailsService userDetailsService(Oauth2Service oauth2Service, PasswordEncoder passwordEncoder) {
return username -> {
List<Oauth2User> users = oauth2Service.getOauth2UserByUsername(username);
if (users == null || users.size() == 0) {
throw new UsernameNotFoundException("username无效");
}
Oauth2User user = users.get(0);
String passwordAfterEncoder = passwordEncoder.encode(user.getPassword());
return User.withUsername(username).password(passwordAfterEncoder).roles("").build();
};
}

标红这句代码大家忽略吧,常理来讲数据库中存储的密码应该就是密文所以这句代码是不需要的,我比较懒数据库直接存储明文密码所以这里需要加密一下。

(3)注册一个ClientDetailsService用户clientId和clientSecret验证

@Bean
public ClientDetailsService clientDetailsService(Oauth2Service oauth2Service, PasswordEncoder passwordEncoder) {
return clientId -> {
List<Oauth2Client> clients1 = oauth2Service.getOauth2ClientByClientId(clientId);
if (clients1 == null || clients1.size() == 0) {
throw new ClientRegistrationException("clientId无效");
}
Oauth2Client client = clients1.get(0);
String clientSecretAfterEncoder = passwordEncoder.encode(client.getClientSecret());
BaseClientDetails clientDetails = new BaseClientDetails();
clientDetails.setClientId(client.getClientId());
clientDetails.setClientSecret(clientSecretAfterEncoder);
clientDetails.setRegisteredRedirectUri(new HashSet<>(Arrays.asList(client.getRedirectUrl().split(","))));
clientDetails.setAuthorizedGrantTypes(Arrays.asList(client.getGrantType().split(",")));
clientDetails.setScope(Arrays.asList(client.getScope().split(",")));
return clientDetails;
};
}

标红代码忽略,理由同上。

关于BaseClientDetails的属性,这里要啰嗦几句:它继承于接口ClientDetails,该接口包含如下属性:

  • getClientId:clientId,唯一标识,不能为空
  • getClientSecret:clientSecret,密码
  • isSecretRequired:是否需要验证密码
  • getScope:可申请的授权范围
  • isScoped:是否需要验证授权范围
  • getResourceIds:允许访问的资源id,这个涉及到资源服务器
  • getAuthorizedGrantTypes:可使用的Oauth2授权模式,不能为空
  • getRegisteredRedirectUri:回调地址,用户在authorization_code模式下接收授权码code
  • getAuthorities:授权,这个完全等同于SpringSecurity本身的授权
  • getAccessTokenValiditySeconds:access_token过期时间,单位秒。null等同于不过期
  • getRefreshTokenValiditySeconds:refresh_token过期时间,单位秒。null等同于getAccessTokenValiditySeconds,0或者无效数字等同于不过期
  • isAutoApprove:判断是否获得用户授权scope

(4)注册一个TokenStore以保存token信息

@Bean
public TokenStore tokenStore(RedisConnectionFactory redisConnectionFactory) {
return new RedisTokenStore(redisConnectionFactory);
}

(5)注册一个AuthorizationCodeServices以保存authorization_code的授权码code

生成一个RandomValueAuthorizationCodeServices的bean,而不是直接生成AuthorizationCodeServices的bean。RandomValueAuthorizationCodeServices可以帮我们完成code的生成过程。如果你想按照自己的规则生成授权码code请直接生成AuthorizationCodeServices的bean。

@Bean
public AuthorizationCodeServices authorizationCodeServices(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, OAuth2Authentication> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.afterPropertiesSet(); return new RandomValueAuthorizationCodeServices() { @Override
protected void store(String code, OAuth2Authentication authentication) {
redisTemplate.boundValueOps(code).set(authentication, 10, TimeUnit.MINUTES);
} @Override
protected OAuth2Authentication remove(String code) {
OAuth2Authentication authentication = redisTemplate.boundValueOps(code).get();
redisTemplate.delete(code);
return authentication;
}
};
}

(6)注册一个AuthenticationManager用来password模式下用户身份认证

直接使用上面注册的UserDetailsService来完成用户身份认证。

@Bean
public AuthenticationManager authenticationManager(UserDetailsService userDetailsService, PasswordEncoder passwordEncoder) {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userDetailsService);
provider.setPasswordEncoder(passwordEncoder);
return new ProviderManager(Collections.singletonList(provider));
}

(7)配置认证服务器

上面注册了这么多bean,到了他们发挥作用的时候了

@Bean
public AuthorizationServerConfigurer authorizationServerConfigurer(UserDetailsService userDetailsService, ClientDetailsService clientDetailsService,
                  TokenStore tokenStore, AuthorizationCodeServices authorizationCodeServices, AuthenticationManager authenticationManager) {
return new AuthorizationServerConfigurer() {
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { } @Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(clientDetailsService);
} @Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.userDetailsService(userDetailsService);
endpoints.tokenStore(tokenStore);
endpoints.authorizationCodeServices(authorizationCodeServices);
endpoints.authenticationManager(authenticationManager);
}
};
}

5. 修改配置文件,配置数据库及redis连接

spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://192.168.2.12:3306/test?characterEncoding=utf8
username: root
password: onceas redis:
host: 192.168.2.12
port: 6379
password: 123456

6.测试

(1)密码模式和客户端模式同上

(2)授权码验证模式

  • 访问 http://127.0.0.1:8080/oauth/authorize?client_id=clientId&response_type=code&scope=scope1 scope2&redirect_uri=http://www.baidu.com,跳转到SpringSecurity默认的登录页面:
  • 输入用户名/密码:admin/admin,点击登录后跳转到确认授权页面:

  • 至少选中一个,然后点击Authorize按钮,跳转到 https://www.baidu.com/?code=tg0GDq,这样我们就拿到了授权码。

  • 通过授权码申请token:

    @Test
    public void token_code() {
    MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
    params.add("grant_type", "authorization_code");
    params.add("code", "tg0GDq");
    String response = restTemplate.withBasicAuth("clientId", "clientSecret").postForObject("/oauth/token", params, String.class);
    System.out.println(response);
    }

(3)刷新token

申请的所有token中都没有返回refresh_token,// todo 2018-11-08 此处留坑

三、自定义页面

1. 自定义用户登录页面

用户登录页面就是SpringSecurity的默认登录页面,所以按照SpringSecurity的规则更改即可,可参照https://www.cnblogs.com/LOVE0612/p/9897647.html里面的相关内容

2. 自定义用户授权页面

用户授权页面是/oauth/authorize转发给/oauth/confirm_access然后才呈现最终页面给用户的。所以想要自定义用户授权页面,用户点击Authorize按钮时会通过form表单发送请求:

Request URL: http://127.0.0.1:8080/oauth/authorize
Request Method: POST FormData
user_oauth_approval: true
scope.scope1: true
scope.scope2: true

所以我们要自定义用户授权页面,我们只要重新定义一个mapping即可并按照上述要求完成post请求即可。

(1)增加pom依赖

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

(2)Controller

@Controller
public class Oauth2Controller { @GetMapping("oauth/confirm_access")
public String authorizeGet() {
return "oauth/confirm_access";
}
}

(3)创建/resources/templates/oauth/confirm_access.html

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>my authorize page</title>
</head>
<body>
<form action="/oauth/authorize" method="post">
<input type="hidden" name="user_oauth_approval" value="true">
<div id="scope"></div>
<input type="submit" value="授权">
</form>
<script>
function getQueryString(name) {
var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)", "i");
var r = window.location.search.substr(1).match(reg);
if (r != null) return unescape(r[2]);
return null;
}
</script>
<script>
var scope = getQueryString("scope"); var scopeList = scope.split(" ");
var html = "";
for (var i = 0; i < scopeList.length; i++) {
html += scopeList[i] + ":<input type='checkbox' name='scope." + scopeList[i] + "' value='true'/><br />";
}
document.getElementById("scope").innerHTML = html;
</script>
</body>
</html>

3. 自定义错误页面

与上面同理,重新定义一个mapping对应uri:/oauth/error,可通过 Object error = request.getAttribute("error"); 获取错误信息,具体html页面内容就不再赘述了。

四、支持Restfull风格

如果考虑前后分离呢?那么流程应该是:

  1. 用户访问第三方client网站
  2. 第三方网站将用户导向我们的某个前端页面地址并携带参数client_id、scope、redirect_uri
  3. 该前端页面通过ajax请求后台接口/oauth/authorize?client_id={client_id}&response_type=code&scope={scope}&redirect_uri={redirect_uri}
  4. 后端接到请求后SpringSecurity首先会校验参数合法性,不合法则转发到/oauth/error,/oauth/error返回jons结果告知前端参数不合法。如果参数合法则再判断当前是否已有用户通过认证:有,则会将请求转发到/oauth/confirm_access,/oauth/confirm_access方法返回json结果告知前端需要用户授权;如果没有则会将请求转发到/login(get请求),/login方法也返回json结果告知前端需要用户登录。
  5. 前端页面根据返回结果判断,如果需要登录则跳转到登录页面,如果需要用户授权则跳转到用户授权页面。
  6. 如果跳转到用户登录页面,用户输入用户名密码点击登录按钮,前端通过ajax请求后台接口/login(post请求),后端接到请求后SpringSecurity判断用户认证是否通过:如果通过则转发请求到and().formLogin().successForwardUrl()所设定的uri,该uri返回json结果告知用户登录成功。如果未通过则转发请求到and().formLogin().failureForwardUrl("/login/error")所设定的uri,该uri返回json结果告知用户登录失败
  7. 前端用户登录页面拿到后端返回的登录结果,如果登录失败则继续等待用户填写用户名密码重新登录,如果登录成功则跳转到用户授权页面。
  8. 用户进行授权勾选并点击确认授权后,前端通过表单post到后台接口/oauth/authorize
  9. 后端接到请求后处理并重定向会第三方client回调地址

SpringBoot实现OAuth2认证服务器的更多相关文章

  1. Spring Cloud实战 | 第九篇:Spring Cloud整合Spring Security OAuth2认证服务器统一认证自定义异常处理

    本文完整代码下载点击 一. 前言 相信了解过我或者看过我之前的系列文章应该多少知道点我写这些文章包括创建 有来商城youlai-mall 这个项目的目的,想给那些真的想提升自己或者迷茫的人(包括自己- ...

  2. Spring cloud微服务安全实战-4-5搭建OAuth2认证服务器

    现在可以访问我们的认证服务器,应用我们已经配置好了. 下面配置让用户可以访问我的认证服务器.再来重写一个方法. EndpointConfigure端点的配置. authenticationManage ...

  3. Spring Cloud微服务安全实战_4-5_搭建OAuth2资源服务器

    上一篇搭建了一个OAuth2认证服务器,可以生成token,这篇来改造下之前的订单微服务,使其能够认这个token令牌. 本篇针对订单服务要做三件事: 1,要让他知道自己是资源服务器,他知道这件事后, ...

  4. oauth2使用心得-----基本概念以及认证服务器搭建

    应用场景 我们假设你有一个“云笔记”产品,并提供了“云笔记服务”和“云相册服务”,此时用户需要在不同的设备(PC.Android.iPhone.TV.Watch)上去访问这些“资源”(笔记,图片) 那 ...

  5. SpringBoot之oauth2.0学习之服务端配置快速上手

    现在第三方登录的例子数见不鲜.其实在这种示例当中,oauth2.0是使用比较多的一种授权登录的标准.oauth2.0也是从oauth1.0升级过来的.那么关于oauth2.0相关的概念及其原理,大家可 ...

  6. Spring Cloud下基于OAUTH2认证授权的实现

    GitHub(spring -boot 2.0.0):https://github.com/bigben0123/uaa-zuul 示例(spring -boot 2.0.0): https://gi ...

  7. OAuth2认证和授权:AuthorizationCode认证

    前面的OAuth2认证,里面的授权服务器都是用的identityserver4搭建的 ids4没有之前一般都是Owin搭建授权服务器,博客园有很多 ids4出来后,一般都是用ids4来做认证和授权了, ...

  8. SpringBoot之OAuth2.0学习之客户端快速上手

    2.1.@EnableOAuth2Sso 这个注解是为了开启OAuth2.0的sso功能,如果我们配置了WebSecurityConfigurerAdapter,它通过添加身份验证过滤器和身份验证(e ...

  9. OAuth2认证有一定的了解

    转到分享界面后,进行OAuth2认证: 以新浪为例: 第一步.WebView加载界面,传递参数 使用WebView加载登陆网页,通过Get方法传递三个参数:应用的appkey.回调地址和展示方式dis ...

随机推荐

  1. mysql设置timpstamp的默认值为 '0000-00-00 00:00:00' 时报错

    问题:mysql设置timpstamp的默认值为 '0000-00-00 00:00:00' 时报错: ERROR 1067 (42000): Invalid default value for 'u ...

  2. Hadoop入门概念

    Hadoop作者:Dong Cutting. 受Google三篇论文的启发. 版本: Apache:官方版本 Cloudera:官方版本的封装,优化,打很多patch,商业版本 HortonWorks ...

  3. NoNodeAvailableException异常的解决

    Elasticsearch 相关学习,昨天还好好的,今天就出错了!!! 完整异常为 : NoNodeAvailableException[None of the configured nodes ar ...

  4. 前端学习笔记系列一:3 Vue中的nextTick

    一.示例 先来一个示例了解下关于Vue中的DOM更新以及nextTick的作用. 模板 <div class="app"> <div ref="msgD ...

  5. mybatis insert后返回主键ID

    需求: mybatis  在添加记录时需要获取到记录主键id id=0 无法获取主键id的值 在插入方法中添加如下属性和相应的值 <insert useGeneratedKeys="t ...

  6. P1061 判断题

    P1061 判断题 转跳点:

  7. Hexo搭建个人博客及next主题基本配置

    前言 国内一些免费的博客平台比如CSDN.博客园都已经很成熟,功能齐全,已经可以满足我们的需求,帮助我们记录学习过程遇到的问题,还能分享帮助其他人解决问题.为什么还要自己动手去搭建博客呢?首先写博客是 ...

  8. Apache http 包中的常量

    org.apache.* org.apache.http.Consts public static final int CR 13 public static final int HT 9 publi ...

  9. SQL计算字符串里的子字符串出现个数

    在某个页面,需要显示每条记录中有几个图片文件.图片文件名列表存储在mysql表里的photo_files字段,文件名之间用一个空格分开.类似'images\rpt201503121.jpg image ...

  10. iPhone Safari下iframe不显示滚动条无法滚动的解决方法。iframe的坑!

    <div class="dataTables_wrapper" style="-webkit-overflow-scrolling:touch;overflow:a ...