<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>
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
@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(); } }; } }
@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); } }
访问 http://127.0.0.1:8080/oauth/authorize?client_id=clientId&response_type=code,跳转到SpringSecurity默认的登录页面:
http://127.0.0.1:8080/oauth/authorize?client_id=clientId&response_type=code
输入用户名/密码:admin/admin,点击登录后跳转到确认授权页面:
至少选中一个,然后点击Authorize按钮,跳转到 https://www.baidu.com/?code=tg0GDq,这样我们就拿到了授权码。
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); }
@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 此处留坑
上面我们搭建的认证服务器存在以下弊端:
clientId和clientSecret
clientId
clientSecret
code
token
针对以上问题,我们要做的就是
接下来我们一步一步实现:
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');
创建测试用表及数据
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
因为要使用到数据库以及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>
这样做的目的是:在我们的应用中,可能都多个地方需要我们对用户的明文密码进行加密。在这里我们统一注册一个PasswordEncoder,以保证加密算法的一致性。
@Bean public PasswordEncoder passwordEncoder() { return PasswordEncoderFactories.createDelegatingPasswordEncoder(); }
@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(); }; }
标红这句代码大家忽略吧,常理来讲数据库中存储的密码应该就是密文所以这句代码是不需要的,我比较懒数据库直接存储明文密码所以这里需要加密一下。
ClientDetailsService
@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,该接口包含如下属性:
@Bean public TokenStore tokenStore(RedisConnectionFactory redisConnectionFactory) { return new RedisTokenStore(redisConnectionFactory); }
AuthorizationCodeServices
authorization_code
生成一个RandomValueAuthorizationCodeServices的bean,而不是直接生成AuthorizationCodeServices的bean。RandomValueAuthorizationCodeServices可以帮我们完成code的生成过程。如果你想按照自己的规则生成授权码code请直接生成AuthorizationCodeServices的bean。
RandomValueAuthorizationCodeServices
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; } }; }
直接使用上面注册的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)); }
上面注册了这么多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); } }; }
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
访问 http://127.0.0.1:8080/oauth/authorize?client_id=clientId&response_type=code&scope=scope1 scope2&redirect_uri=http://www.baidu.com,跳转到SpringSecurity默认的登录页面:
申请的所有token中都没有返回refresh_token, // todo 2018-11-08 此处留坑
用户登录页面就是SpringSecurity的默认登录页面,所以按照SpringSecurity的规则更改即可,可参照https://www.cnblogs.com/LOVE0612/p/9897647.html里面的相关内容
用户授权页面是/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请求即可。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>
@Controller public class Oauth2Controller { @GetMapping("oauth/confirm_access") public String authorizeGet() { return "oauth/confirm_access"; } }
<!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>
与上面同理,重新定义一个mapping对应uri:/oauth/error,可通过 Object error = request.getAttribute("error"); 获取错误信息,具体html页面内容就不再赘述了。
如果考虑前后分离呢?那么流程应该是:
原文链接:https://www.cnblogs.com/LOVE0612/p/9913336.html