SpringBoot 实现 OAuth2 认证服务

2020-04-01  本文已影响0人  xin_5457

本文使用spring-security-oauth2实现OAuth2认证服务。
源码地址:https://github.com/kangarooxin/spring-security-oauth2-demo

  1. pom.xml加入依赖
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.security.oauth</groupId>
            <artifactId>spring-security-oauth2</artifactId>
            <version>2.3.8.RELEASE</version>
        </dependency>
  1. 配置SpringSecurity
@Configuration
@EnableWebSecurity //启用SpringSecurity
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    /**
     * 配置Security处理授权服务器相关请求
     * 
     * @param httpSecurity
     * @throws Exception
     */
    @Override
    public void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity.requestMatchers().antMatchers("/oauth/**", "/login", "/logout")//配置需要Security拦截的请求
                .and().formLogin().permitAll().loginPage("/login")//指定登录页面
                .and().logout().logoutUrl("/logout")//指定登出页面
                .and().authorizeRequests().anyRequest().authenticated()//其它页面都需要鉴权认证
                .and().csrf().disable()//不开启csrf
        ;
    }

    /**
     * 用户服务
     * 
     * 此处在内存创建了2个用户,实际场景替换为db服务查询,只需要实现UserDetailsService即可
     * 
     * @return
     */
    @Bean
    @Override
    public UserDetailsService userDetailsService() {
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(User.withUsername("user1").password(passwordEncoder.encode("123456")).authorities("USER").build());
        manager.createUser(User.withUsername("user2").password(passwordEncoder.encode("123456")).authorities("USER").build());
        return manager;
    }

    /**
     * 设置密码校验器
     * NoOpPasswordEncoder 直接文本比较  equals
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
  1. 配置认证服务AuthorizationServer
@Configuration
@EnableAuthorizationServer //启用认证服务
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private PasswordEncoder passwordEncoder;

    /**
     * *用来配置令牌端点(Token Endpoint)的安全约束。
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.allowFormAuthenticationForClients()//默认/oauth/token认证是使用BasicAuth,此处设置允许通过表单URL提交client_id和client_secret。
                .passwordEncoder(passwordEncoder)//设置密码编码器, 使用client_id登录时,密码加密要跟用户加密一致。
                .checkTokenAccess("isAuthenticated()")//开启/oauth/check_token验证端口认证权限访问
        ;
    }

    /**
     * 配置OAuth2的客户端相关信息
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.withClientDetails(oauth2ClientDetailsService());
    }

    /**
     * 配置授权服务器端点的属性
     *
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.authenticationManager(authenticationManager)
                .tokenStore(oauth2TokenStore())//指定token存储服务
                .authorizationCodeServices(authorizationCodeServices())//指定code生成服务
                .userDetailsService(userDetailsService)//用户服务
                .reuseRefreshTokens(false)//每次刷新token会创建新的refresh_token
        ;
    }

    /**
     * 设置令牌存储方式
     * InMemoryTokenStore 在内存中存储令牌。
     * RedisTokenStore 在Redis缓存中存储令牌。
     * JwkTokenStore 支持使用JSON Web Key (JWK)验证JSON Web令牌(JwT)的子Web签名(JWS)
     * JwtTokenStore 不是真正的存储,不持久化数据,身份和访问令牌可以相互转换。
     * JdbcTokenStore 在数据库存储,需要创建相应的表存储数据
     */
    @Bean
    public TokenStore oauth2TokenStore() {
        return new InMemoryTokenStore();
    }

    /**
     * 设置Code生成服务
     * 
     * @return
     */
    @Bean
    public AuthorizationCodeServices authorizationCodeServices() {
        return new InMemoryAuthorizationCodeServices();
    }

    /**
     * client服务
     * 
     * @return
     * @throws Exception
     */
    @Bean
    public ClientDetailsService oauth2ClientDetailsService() throws Exception {
        InMemoryClientDetailsServiceBuilder builder = new InMemoryClientDetailsServiceBuilder();
        builder.withClient("clientId")
                .secret(passwordEncoder.encode("clientSecret"))
                .scopes("scope1", "scope2")
                .authorizedGrantTypes("client_credentials", "password", "authorization_code", "refresh_token")//支持的授权类型
                .redirectUris("http://www.baidu.com") //回调url
                .autoApprove(false) //允许自动授权
                .accessTokenValiditySeconds((int)TimeUnit.HOURS.toSeconds(12))
                .refreshTokenValiditySeconds((int)TimeUnit.DAYS.toSeconds(30))
        ;
        return builder.build();
    }
}
  1. 配置资源服务ResourceServer
@Configuration
@EnableResourceServer //启用资源服务
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.requestMatchers().antMatchers("/api/**") //仅拦截资源服务相关请求
                .and().authorizeRequests()
                .antMatchers("/api/group1/**").access("#oauth2.hasScope('scope1')") //配置scope访问权限
                .antMatchers("/api/group2/**").access("#oauth2.hasScope('scope2')")
                .anyRequest().authenticated();
    }
}
  1. 创建资源API
@RestController
@RequestMapping("/api/group1")
public class Group1Controller {

    @RequestMapping
    public String hello() {
        return "hello group1";
    }
}
@RestController
@RequestMapping("/api/group2")
public class Group2Controller {

    @RequestMapping
    public String hello() {
        return "hello group2";
    }
}
  1. 授权测试

一般用于资源服务器是应用的一个后端模块,客户端向认证服务器验证身份来获取令牌。

curl -d "grant_type=client_credentials&client_id=clientId&client_secret=clientSecret" http://localhost:8080/oauth/token
{
  "access_token" : "6c246e41-7d9a-4b69-b340-893635638ed8",
  "token_type" : "bearer",
  "expires_in" : 42444,
  "scope" : "scope1"
}
curl -d "grant_type=password&username=username&password=password&client_id=clientId&client_secret=clientSecret" http://localhost:8080/oauth/token
{
  "access_token" : "b7ca73bc-992c-4c23-abbd-f819aa220199",
  "token_type" : "bearer",
  "refresh_token" : "51661039-fb67-4e6b-a563-bd4c58b084c0",
  "expires_in" : 43199,
  "scope" : "scope1"
}
  1. 访问测试
    http://127.0.0.1:8080/api/group1?access_token=39a2536a-d420-4798-abb3-1fc083cce887

使用Redis存储AuthorizationCode

    @Bean
    public AuthorizationCodeServices authorizationCodeServices() {
        return new RandomValueAuthorizationCodeServices() {

            private static final String AUTH_CODE_KEY = "auth_code";

            @Override
            protected void store(String code, OAuth2Authentication authentication) {
                RedisConnection conn = getConnection();
                try {
                    conn.hSet(AUTH_CODE_KEY.getBytes(StandardCharsets.UTF_8), code.getBytes(StandardCharsets.UTF_8),
                            SerializationUtils.serialize(authentication));
                } catch (Exception e) {
                    log.error("save authentication code " + code + " failed:", e);
                } finally {
                    conn.close();
                }
            }

            @Override
            protected OAuth2Authentication remove(String code) {
                RedisConnection conn = getConnection();
                try {
                    OAuth2Authentication authentication;
                    try {
                        byte[] bytes = conn.hGet(AUTH_CODE_KEY.getBytes(StandardCharsets.UTF_8), code.getBytes(StandardCharsets.UTF_8));
                        if(bytes == null) {
                            return null;
                        }
                        authentication = SerializationUtils.deserialize(bytes);
                    } catch (Exception e) {
                        return null;
                    }

                    if (null != authentication) {
                        conn.hDel(AUTH_CODE_KEY.getBytes(StandardCharsets.UTF_8), code.getBytes(StandardCharsets.UTF_8));
                    }

                    return authentication;
                } catch (Exception e) {
                    log.error("remove authentication code " + code + "failed:", e);
                    return null;
                } finally {
                    conn.close();
                }
            }

            private RedisConnection getConnection() {
                return redisConnectionFactory.getConnection();
            }
        };
    }

自定义登录和授权页面

  1. pom.xml添加依赖
      <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
  1. 创建Oauth2Controller
@Controller
@RequestMapping("/oauth")
public class Oauth2Controller {

    //登录页面
    @GetMapping("login")
    public String login() {
        return "oauth/login";
    }
    //授权页
    @GetMapping("confirm_access")
    public String authorizeGet() {
        return "oauth/confirm_access";
    }
    //授权错误页
    @GetMapping("error")
    public String error() {
        return "oauth/error";
    }
}
  1. 在templates下创建页面模板
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登录</title>
</head>
<body>
<form method="post" action="/oauth/login">
  用户名:<input name="username" placeholder="请输入用户名" type="text">
  密码:<input name="password" placeholder="请输入密码" type="password">
  <input value="登录" type="submit">
</form>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>授权</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>
${error}
上一篇下一篇

猜你喜欢

热点阅读