单点登录 Oauth2.0 + jwt + 资源服务器配置与授权

一、创建认证配置类

1. pom

<properties>
    <java.version>1.8</java.version>
    <spring-cloud.version>Greenwich.SR1</spring-cloud.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-autoconfigure</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <version>2.0.2.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-freemarker</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
        <scope>provided</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-oauth2-client</artifactId>
        <scope>provided</scope>
        </dependency>
    <dependency>
        <groupId>org.springframework.security.oauth.boot</groupId>
        <artifactId>spring-security-oauth2-autoconfigure</artifactId>
        <version>2.1.3.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.security.oauth</groupId>
        <artifactId>spring-security-oauth2</artifactId>
        <version>2.3.3.RELEASE</version>
        <scope>compile</scope>
    </dependency>
    <dependency>
        <groupId>com.pcy</groupId>
        <artifactId>pcy-common</artifactId>
        <version>0.6.2-SNAPSHOT</version>
        <scope>compile</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.security.oauth</groupId>
        <artifactId>spring-security-oauth2</artifactId>
        <version>2.3.3.RELEASE</version>
        <scope>compile</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-config</artifactId>
    </dependency>
</dependencies>

<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>

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <configuration>
                <mainClass>com.funtl.oauth2.OAuth2ServerApplication</mainClass>
            </configuration>
        </plugin>
    </plugins>
</build>

2.集成AuthorizationServerConfigurerAdapter,添加@EnableAuthorizationServer直接,并重写configure方法

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {

    private AuthenticationManager authenticationManager;
    private TokenStore tokenStore;
    private AccessTokenConverter accessTokenConverter;
    private Oauth2UserDetailsService oauth2UserDetailsService;
    private JwtAccessTokenConverter jwtAccessTokenConverter;

    @Autowired
    private BCryptPasswordEncoder passwordEncoder;

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        //通过实现 ClientDetailsService 来进行动态创建
        clients.withClientDetails(new BaseClientDetailService());
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        //允许表单提交
        security.allowFormAuthenticationForClients()
                .checkTokenAccess("permitAll()")
                .tokenKeyAccess("permitAll()");
    }

    /**
     * jwt访问token转换器
     * @return
     */
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter(){
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        Resource resource = new ClassPathResource("public.cert");//秘钥
        String publicKey;
        try {
            publicKey = new String(FileCopyUtils.copyToByteArray(resource.getInputStream()));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        converter.setVerifierKey(publicKey);
        return converter;
    }

    /**
     * 设置token 由Jwt产生,不使用默认的透明令牌
     */
    @Bean
    public JwtTokenStore jwtTokenStore() {
        return new JwtTokenStore(jwtAccessTokenConverter());
    }


    /**
     * 加密token
     * @param endpoints
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        //      将增强的token设置到增强链中
        TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
        enhancerChain.setTokenEnhancers(Arrays.asList(customTokenEnhancer(), jwtAccessTokenConverter()));

        endpoints
                .authenticationManager(authenticationManager)
                .tokenStore(jwtTokenStore())
                //.accessTokenConverter(accessTokenConverter())
                .accessTokenConverter(jwtAccessTokenConverter())
                .allowedTokenEndpointRequestMethods(HttpMethod.GET,
                        HttpMethod.POST)
                .tokenEnhancer(enhancerChain);//将增强token配置
    }

    /**
     * 加载令牌增强器
     * @return
     */
    @Bean
    public TokenEnhancer customTokenEnhancer() {
        return new CustomTokenEnhancer();
    }
}

3. 自定义客户端认证

public class BaseClientDetailService implements ClientDetailsService {

    @Autowired
    private BCryptPasswordEncoder passwordEncoder;

    @Override
    public ClientDetails loadClientByClientId(String clientId) throws ClientRegistrationException {
        System.out.println(clientId);
        BaseClientDetails client = null;
        //可以改为查询数据库
        if("client".equals(clientId)) {
            client = new BaseClientDetails();
            client.setClientId(clientId);
            client.setClientSecret(passwordEncoder.encode("secret"));
            //client.setResourceIds(Arrays.asList("order"));
            client.setAuthorizedGrantTypes(Arrays.asList("authorization_code","client_credentials","refresh_token","password","implicit"));
            //不同的client可以通过 一个scope 对应 权限集
            client.setScope(Arrays.asList("authorization_code", "refresh_token","app"));
            client.setAuthorities(AuthorityUtils.createAuthorityList("admin_role"));
            client.setAccessTokenValiditySeconds((int) TimeUnit.DAYS.toSeconds(1)); //1天
            client.setRefreshTokenValiditySeconds((int)TimeUnit.DAYS.toSeconds(1)); //1天
            Set<String> uris = new HashSet<>();
            uris.add("http://localhost:8080/static/home-page.html");//回调地址,访问登录的路径中的回调地址需要再uris中匹配
            client.setRegisteredRedirectUri(uris);
            client.setAutoApproveScopes(Arrays.asList("authorization_code", "refresh_token","app"));

        }
        if(client == null) {
            throw new NoSuchClientException("No client width requested id: " + clientId);
        }
        return client;
    }
}

4.WebSecurityConfigurerAdapter
WebSecurityConfigurerAdapter 类是个适配器, 在配置的时候,需要我们自己写个配置类去继承他,然后编写自己所特殊需要的配置。

@EnableWebSecurity是Spring Security用于启用Web安全的注解。典型的用法是该注解用在某个Web安全配置类上(实现了接口WebSecurityConfigurer或者继承自WebSecurityConfigurerAdapter)

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {

    private final Oauth2UserDetailsService oauth2UserDetailsService;

    private final PasswordEncoder passwordEncoder;

    public WebSecurityConfiguration(Oauth2UserDetailsService oauth2UserDetailsService, PasswordEncoder passwordEncoder) {
        this.oauth2UserDetailsService = oauth2UserDetailsService;
        this.passwordEncoder = passwordEncoder;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception{
        // 默认支持./login实现authorization_code认证
        http
            .formLogin().loginPage("/index").loginProcessingUrl("/authentication/form")
            .and()
            .authorizeRequests()
            .antMatchers("/index", "/login", "/resources/**", "/static/**","/authorizationtoken/**","/oauth/token").permitAll()
            .anyRequest() // 任何请求
            .authenticated()// 都需要身份认证
            //.and()
            //.logout().invalidateHttpSession(true).deleteCookies("JSESSIONID").logoutSuccessHandler(customLogoutSuccessHandler).permitAll()
            .and()
            .csrf().disable();
    }

    /**
     * 获取用户信息
     */
    @Bean
    public UserDetailsService userDetailsService() {
        return new Oauth2UserDetailsService();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(oauth2UserDetailsService).passwordEncoder(passwordEncoder);
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        // 将 check_token 暴露出去,否则资源服务器访问时报 403 错误
        web.ignoring().antMatchers("/oauth/check_token");
    }
}

5.加密

@Configuration
public class Oauth2BeanConfig {
    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        // 设置默认的加密方式
        return new BCryptPasswordEncoder();
    }
}

6.身份认证自定义
框架提供一个UserDetailsService接口用来加载用户信息。如果要自定义实现的话,用户可以实现一个CustomUserDetailsService的类,然后把你的应用中的UserService和AuthorityService注入到这个类中,用户获取用户信息和权限信息,然后在loadUserByUsername方法中,构造一个User对象(框架的类)返回即可。

@Service
public class Oauth2UserDetailsService implements UserDetailsService {

    @Autowired
    private BCryptPasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        try {
            //此处应该通过username在数据库中查询userinfo信息 返回userDetails之后spring security会自动对返回UserDetails得密码与登录页面的密码进行加密匹配
            UserInfoDto user = new UserInfoDto();
            user.setUsername("pangchunyou");
            user.setPassword(passwordEncoder.encode("123456"));
            Collection<GrantedAuthority> authList = getAuthorities();
            UserDetails userDetails = user.toUserInfo();
            return userDetails;
        }catch (Exception e) {
            throw new UsernameNotFoundException("登录失败");
        }

    }

    /**  * 获取用户的角色权限,为了降低实验的难度,这里去掉了根据用户名获取角色的步骤     * @param    * @return   */
    private Collection<GrantedAuthority> getAuthorities(){
        List<GrantedAuthority> authList = new ArrayList<GrantedAuthority>();
        authList.add(new SimpleGrantedAuthority("ROLE_USER"));
        authList.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
        return authList;
    }
}

7.令牌增强器

/**
 * 实现TokenEnhancer向jwt中添加额外信息  令牌增强器
 */
public class CustomTokenEnhancer implements TokenEnhancer {
    @Override
    public OAuth2AccessToken enhance(OAuth2AccessToken oAuth2AccessToken, OAuth2Authentication oAuth2Authentication) {
        User user = (User) oAuth2Authentication.getPrincipal();
        final Map<String, Object> additionalInfo = new HashMap<>();
        additionalInfo.put("customInfo", "some_stuff_here");
        additionalInfo.put("password",user.getPassword());
        UserInfo userInfo = (UserInfo) oAuth2Authentication.getPrincipal();
        additionalInfo.put("state",userInfo.getState());
        additionalInfo.put("userinfo",oAuth2Authentication.getPrincipal().toString());
        ((DefaultOAuth2AccessToken) oAuth2AccessToken).setAdditionalInformation(additionalInfo);
        return oAuth2AccessToken;
    }
}

8.指定签名

@Configuration
public class JwtTokenConfig {

    @Bean
    public TokenStore jwtTokenStore(){
        return new JwtTokenStore(jwtAccessTokenConverter());
    }

    /**
     * token生成处理:指定签名
     */
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter(){
        JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter();
        accessTokenConverter.setSigningKey("internet_plus");
        return accessTokenConverter;
    }
}

9.用户实体类

public class UserInfo extends User implements OAuth2User {
    private String userId;
    private Collection<String> orgIds;
    private String name;
    private Collection<String> roleIds;
    private OAuth2AccessToken oAuth2AccessToken;
    private Map<String, Object> attributes;
    private String state = "111";

    public UserInfo(String username) {
        super(username, "", Collections.emptyList());
    }

    public UserInfo(String username, String paasword) {
        super(username, paasword, Collections.emptyList());
    }

    public UserInfo(String username, String userId, String name, Collection<String> roleIds) {
        super(username, "", Collections.emptyList());
        this.name = name;
        this.userId = userId;
        this.roleIds = roleIds;
    }

    public UserInfo(String username, String paasword, String userId, String name, Collection<String> roleIds) {
        super(username, paasword, Collections.emptyList());
        this.name = name;
        this.userId = userId;
        this.roleIds = roleIds;
    }

    public Collection<String> getOrgIds() {
        return this.orgIds;
    }

    public void setOrgIds(Collection<String> orgIds) {
        this.orgIds = orgIds;
    }

    public String getUserId() {
        return this.userId;
    }

    public Collection<String> getRoleIds() {
        return this.roleIds;
    }

    public void setAttributes(Map<String, Object> attributes) {
        this.attributes = attributes;
    }

    public String getUsername() {
        return super.getUsername();
    }

    public Map<String, Object> getAttributes() {
        return this.attributes;
    }

    public String getName() {
        return StringUtils.isEmpty(this.name) ? super.getUsername() : this.name;
    }

    public OAuth2AccessToken getoAuth2AccessToken() {
        return this.oAuth2AccessToken;
    }

    public void setoAuth2AccessToken(OAuth2AccessToken oAuth2AccessToken) {
        this.oAuth2AccessToken = oAuth2AccessToken;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }

    public void setUserId(String userId) {
        this.userId = userId;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setRoleIds(Collection<String> roleIds) {
        this.roleIds = roleIds;
    }

    public String getState() {
        return state;
    }

    public void setState(String state) {
        this.state = state;
    }
}

10. token转换实体类

public class AuthorizationAccessToken {
    @JsonProperty("access_token")
    private String accessToken;
    @JsonProperty("token_type")
    private String tokenType;
    @JsonProperty("refresh_token")
    private String refreshToken;
    @JsonProperty("expires_in")
    private Integer expiresIn;
    private String scope;
    private String jti;

    public AuthorizationAccessToken() {
    }

    public String getAccessToken() {
        return this.accessToken;
    }

    public void setAccessToken(String accessToken) {
        this.accessToken = accessToken;
    }

    public String getTokenType() {
        return this.tokenType;
    }

    public void setTokenType(String tokenType) {
        this.tokenType = tokenType;
    }

    public String getRefreshToken() {
        return this.refreshToken;
    }

    public void setRefreshToken(String refreshToken) {
        this.refreshToken = refreshToken;
    }

    public Integer getExpiresIn() {
        return this.expiresIn;
    }

    public void setExpiresIn(Integer expiresIn) {
        this.expiresIn = expiresIn;
    }

    public String getScope() {
        return this.scope;
    }

    public void setScope(String scope) {
        this.scope = scope;
    }

    public String getJti() {
        return this.jti;
    }

    public void setJti(String jti) {
        this.jti = jti;
    }
}

11.token获取

@GetMapping("/token")
public ResponseResult token(String code) {
    HttpEntity<MultiValueMap<String, String>> httpEntity = this.oauth2TokenHttpEntity(code);
    ResponseEntity<AuthorizationAccessToken> responseEntity = this.restTemplate.postForEntity(TOKEN_URI, httpEntity, AuthorizationAccessToken.class, new Object[0]);
    AuthorizationAccessToken authorizationAccessToken = (AuthorizationAccessToken)responseEntity.getBody();
    return renderSuccess(authorizationAccessToken);
}

public HttpEntity<MultiValueMap<String, String>> oauth2TokenHttpEntity(String code) {
        HttpHeaders headers = new HttpHeaders();
        MultiValueMap<String, String> map = new LinkedMultiValueMap();
        map.add("grant_type", "authorization_code");
        map.add("code", code);
        map.add("redirect_uri", REDIRECT_URLI);
        map.add("client_id", CLIENT_ID);
        map.add("client_secret", CLIENT_SECRET);
        HttpEntity<MultiValueMap<String, String>> httpEntity = new HttpEntity(map, headers);
        return httpEntity;
    }

二、创建资源服务器

认证服务器搭建完成,接下来需要对资源服务器进行开启资源保护,对未授权的请求转跳到单点登录页面

1. pom

<dependency>
    <groupId>org.springframework.security.oauth</groupId>
    <artifactId>spring-security-oauth2</artifactId>
    <version>2.3.5.RELEASE</version>
    <scope>compile</scope>
</dependency>
<dependency>
    <groupId>org.springframework.security.oauth.boot</groupId>
    <artifactId>spring-security-oauth2-autoconfigure</artifactId>
    <version>2.1.3.RELEASE</version>
    <scope>compile</scope>
</dependency>

2.启动类添加@ EnableOAuth2Sso注解

该注解将您的服务标记为OAuth 2.0客户。它将负责将资源所有者重定向到用户必须输入其凭据的授权服务器。然后用户将被重定向回具有授权码的客户端。然后客户端通过调用授权服务器获取授权代码并将其交换为访问令牌。之后,客户端才能使用访问令牌去调用资源服务器。

@SpringBootApplication
@EnableDiscoveryClient
@EnableOAuth2Sso
public class ResourceApplication {

    public static void main(String[] args) {
        SpringApplication.run(ResourceApplication.class, args);
    }

}

3. 创建配置类

@Configuration
@EnableResourceServer
public class ResourceServerConfigurer extends ResourceServerConfigurerAdapter {
    @Override
    public void configure(HttpSecurity http) throws Exception {
        // 对 所有 请求进行拦截
        //http.authorizeRequests().antMatchers("/**").authenticated();
        //对/test1之外的请求进行拦截
        http
                .authorizeRequests()
                .antMatchers("/test1").permitAll()
                .anyRequest() // 任何请求
                .authenticated();// 都需要身份认证
    }

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        //自定义authenticationEntryPoint,实现如果没有访问权限直接跳转到登录页。
        resources.authenticationEntryPoint(new AuthExceptionEntryPoint());
    }
}

4. authenticationEntryPoint校验权限

通过自定义authenticationEntryPoint类来进行资源服务器登录跳转到认证服务器

若请求无访问资源权限,则进入该authenticationEntryPoint,根据自定义的登录地址进行跳转,可在commence进行逻辑处理。

/**
 * 自定义个一个authenticationEntryPoint,实现如果无访问权限跳转到登录页面
 * @author pcy
 */
public class AuthExceptionEntryPoint implements AuthenticationEntryPoint {

    @Value("${security.oauth2.login-url}")
    private String loginUrl;

    @Override
    public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        try {
            System.out.println("进入Entry方法");
            httpServletResponse.sendRedirect(loginUrl+httpServletRequest.getRequestURL());
        } catch (Exception e1) {
            throw new ServletException();
        }
    }
}

版权声明:本文为weixin_44816511原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。