单点登录(Oauth2方式)

目录

一  认证授权业务层

1、用户登录服务

1.1 用户信息远程调用

1.1.1 业务描述

1.1.2 业务逻辑代码

1.2 用户信息业务处理

1.2.1 业务描述

1.2.2 业务逻辑代码

2、认证授权配置类

2.1 令牌配置

2.1.1 定义令牌签名key

2.1.2 配置存储策略

2.1.3 配置令牌创建及验签方式

2.1.4 完整代码参考

2.2 安全配置

2.2.1 定义安全配置对象

2.2.2 定义加密方式

2.2.3 定义认证管理对象

2.2.4 完整代码参考

2.3 授权配置

2.3.1 定义认证授权配置对象

2.3.2 为谁认证(客户端信息配置)

2.3.3 到哪认证(授权安全配置)

2.3.4 如何认证(Oauth2认证方式配置)

2.3.5 完整参考代码

二 资源服务业务层

1、资源配置类

1.1 令牌配置

1.2 资源服务配置

1.2.1 构建资源服务配置对象

1.2.2 重写resource配置方法

1.2.3 重写http配置方法

1.2.4 自定义异常处理(http配置方法异常处理需要此配置)

2.5 完整参考代码

2、授权业务处理

参考代码


单点登录,英文是 Single Sign On(缩写为 SSO)。即多个站点共用一台认证授权服务器,用户在其中任何一个站点登录后,可以免登录访问其他所有站点。而且,各站点间可以通过该登录状态直接交互。

一  认证授权业务层

1、用户登录服务

1.1 用户信息远程调用

1.1.1 业务描述

获取用户信息,基于feign方式建立远程服务调用。

1.1.2 业务逻辑代码

feign方式远程调用步骤:添加openfeign依赖-->启动类上添加 @EnableFeignClients注解 -->接口应用上添加 @FeignClient注解。

注意:@PathVariable注解内需要指定一个值。

参考代码:

启动类上添加:@EnableFeignClients注解

@SpringBootApplication()
@EnableFeignClients
public class AuthApplication {
    public static void main(String[] args) {
        SpringApplication.run(AuthApplication.class,args);
    }
}

 服务接口上添加: @FeignClient注解

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import java.util.List;
@FeignClient(
        value = "manage",
        contextId = "RemoteUserService",
        fallbackFactory = RemoteProviderFallbackFactory.class)
@RequestMapping("/user")
public interface RemoteUserService {
    @GetMapping("/login/{username}")
    User doSelectUserByUsername(@PathVariable("username") String username);

    @GetMapping("/permission/{userId}")
    List doSelectUserPermissionByUserId(@PathVariable("userId") Integer UserId);
}

@FeignClient注解内参数说明:

①: value:调用的远程服务名。

②: contextId:bean名称。

@FeignClient描述的接口底层会为其创建实现类。默认会采用value的值作为bean的名称,为避免bean重名导致调用对象异常,指定一个contextId的值,一般使用接口应用的名称首字母小写作为contextId的值。

③: fallbackFactory:自定义异常处理对象。

import feign.hystrix.FallbackFactory;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class RemoteProviderFallbackFactory implements FallbackFactory {
    @Override
    public RemoteProviderService create(Throwable throwable) {
        log.error("服务调用失败:{}",throwable.getMessage());
        return msg -> "服务忙,稍等片刻在访问";
    }
}

1.2 用户信息业务处理

1.2.1 业务描述

获取登录用户信息,并进行封装。

1.2.2 业务逻辑代码

1、业务类实现UserDetailsService对象,重写loadUserByUsername()方法。

2、注入令牌加密对象 和 远程调用服务对象。

参考代码:

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.List;
@Slf4j
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    @Autowired
    BCryptPasswordEncoder bCryptPasswordEncoder;
    @Autowired
    RemoteUserService remoteUserService;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User userinfo = remoteUserService.doSelectUserByUsername(username);
        if (userinfo == null){
            throw new UsernameNotFoundException("用户不存在");
        }
        List userPermission = remoteUserService.doSelectUserPermissionByUserId(userinfo.getId());
        log.info("userPermission:{}",userPermission);
        return new org.springframework.security.core.userdetails.User(
                username,userinfo.getPassword(),
                AuthorityUtils.createAuthorityList(userPermission.toArray(new String[]{})));
    }
}

2、认证授权配置类

2.1 令牌配置

2.1.1 定义令牌签名key

JWT令牌签名时使用的秘钥(可以理解为盐值加密中的盐)。

private static final String SIGNING_KEY="AUTH";

2.1.2 配置存储策略

存储策略有JDBC,Redis,JWT等方式,这里采用的是JWT方式存储。

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

2.1.3 配置令牌创建及验签方式

基于此对象创建的令牌信息会封装到Oauth2AccessToken类型的对象中 ,然后存储到TokenStore对象,外界需要时,会从tokenStore进行获取。

    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter(){
        JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
        jwtAccessTokenConverter.setSigningKey(SIGNING_KEY);
        return jwtAccessTokenConverter;
    }

2.1.4 完整代码参考

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;

@Configuration
public class TokenConfig {

    private static final String SIGNING_KEY="AUTH";

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

    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter(){
        JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
        jwtAccessTokenConverter.setSigningKey(SIGNING_KEY);
        return jwtAccessTokenConverter;
    }
}

2.2 安全配置

2.2.1 定义安全配置对象

继承 WebSecurityConfigurerAdapter类,重写里面的 configure(HttpSecurity http) 方法。

***这里放行所有资源的访问(后续可以基于选择对资源进行认证和放行)***

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //释放相关请求
        http.authorizeRequests().anyRequest().permitAll();
    }

2.2.2 定义加密方式

底层默认采用的是UUID加密方式。这里采用 BCryptPasswordEncoder 对象方式,这是一种不可逆的加密方式,相对于UUID要更安全。

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder(){
        return new BCryptPasswordEncoder();
    }

2.2.3 定义认证管理对象

这个对象负责完成用户信息的认证,注意定义AuthenticationManager对象把它交给spring容器管理时,要用 authenticationManagerBean() 带 bean的这个方法。

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

2.2.4 完整代码参考

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().anyRequest().permitAll();
    }

}

2.3 授权配置

思路:为谁认证-->到哪认证-->如何认证

2.3.1 定义认证授权配置对象

①:添加  @EnableAuthorizationServer 注解,开启认证授权服务。

②:继承 AuthorizationServerConfigurerAdapter 对象,重写里面的配置方法,注入认证管理对象(AuthenticationManager)、创建令牌对象(JwtAccessTokenConverter)、令牌存储对象(TokenStore)、用户信息对象(UserDetailsService)、令牌加密方式(BCryptPasswordEncoder)。

注入对象时可以使用全参构造把对象封装到一起,省略每个对象上面都要写一遍@Autowired注解(构造方法上默认有@Autowired注解)。

@EnableAuthorizationServer //开启授权
public class Oauth2Config extends AuthorizationServerConfigurerAdapter {
    private BCryptPasswordEncoder bCryptPasswordEncoder;
    private AuthenticationManager authenticationManagerBean;
    private UserDetailsService userDetailsServiceImpl;
    private TokenStore jwtTokenStore;
    private JwtAccessTokenConverter jwtAccessTokenConverter;

2.3.2 为谁认证(客户端信息配置)

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                .withClient("gateway_client")
                .secret(bCryptPasswordEncoder.encode("123456"))
                .scopes("all")
                .authorizedGrantTypes("password","refresh_token");
    }

2.3.3 到哪认证(授权安全配置)

定义令牌要认证、检查的URL。(思考:1.我们输入了用户名和密码,会提交到哪里(URL)?提交到的这个路径是否需要认证? 2.令牌过期了,哪个路径可以帮我们重新生成一个令牌?)

配置:

①:定义(公开)要认证的url (permitAll() 是官方定义好的);

②:定义(公开)令牌检查的url;

③:允许直接通过表单方式由客户端提交认证。

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.tokenKeyAccess("permitAll()")
                .checkTokenAccess("permitAll()")
                .allowFormAuthenticationForClients();
    }

2.3.4 如何认证(Oauth2认证方式配置)

1、Oauth2 定义了一套认证规范,例如:为谁发令牌,都发什么内容。。

配置:

①:由谁完成认证?(认证管理器)。

②:谁负责访问数据库?(需要两部分信息:a.来自客户端;b.来自数据库)。

③:支持对什么请求进行认证?(默认支持post方式)。

④:认证成功以后令牌如何生成和存储?(默认令牌生成是UUID.randomUUID(),并且存储方式是内存)。

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.authenticationManager(authenticationManagerBean)
                .userDetailsService(userDetailsServiceImpl) //谁来负责访问
                .allowedTokenEndpointRequestMethods(HttpMethod.GET,HttpMethod.POST,HttpMethod.PUT,HttpMethod.DELETE)
                .tokenServices(authorizationServerTokenServices());//令牌如何生成、如何存储
    }

2、 构建一个AuthorizationServerTokenServices 类型的bean,返回一个封装好的DefaultTokenServices对象,此对象提供了创建,获取,刷新token的方法。将这个对象作为参数传给 tokenServices()方法。

配置:

①:设置令牌生成策略。

②:设置是否支持令牌刷新(访问令牌过期了,是否支持通过令牌刷新机制,延迟令牌有效期)。

③:设置令牌增强(默认令牌会比较简单,没有业务数据,就是简单的随机字符串,担现在希望使用JWT的方式)。

④:设置令牌访问有效期。

⑤:设置令牌刷新有效期。

    @Bean
    public AuthorizationServerTokenServices authorizationServerTokenServices(){
        //构建TokenService对象(此对象提供了创建,获取,刷新token的方法)
        DefaultTokenServices defaultTokenServices = new DefaultTokenServices();

        defaultTokenServices.setTokenEnhancer(jwtAccessTokenConverter);
        defaultTokenServices.setTokenStore(jwtTokenStore);
        defaultTokenServices.setSupportRefreshToken(true);
        defaultTokenServices.setAccessTokenValiditySeconds(3600);
        defaultTokenServices.setRefreshTokenValiditySeconds(3600*24);
        return defaultTokenServices;
    }

2.3.5 完整参考代码

import lombok.AllArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;

@Configuration
@AllArgsConstructor
@EnableAuthorizationServer //开启授权
public class Oauth2Config extends AuthorizationServerConfigurerAdapter {
    private BCryptPasswordEncoder bCryptPasswordEncoder;
    private AuthenticationManager authenticationManagerBean;
    private UserDetailsService userDetailsServiceImpl;
    private TokenStore jwtTokenStore;
    private JwtAccessTokenConverter jwtAccessTokenConverter;

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                .withClient("gateway_client")
                .secret(bCryptPasswordEncoder.encode("123456"))
                .scopes("all")
                .authorizedGrantTypes("password","refresh_token");
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.tokenKeyAccess("permitAll()")
                .checkTokenAccess("permitAll()")
                .allowFormAuthenticationForClients();
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.authenticationManager(authenticationManagerBean)
                .userDetailsService(userDetailsServiceImpl) //谁来负责访问
                .allowedTokenEndpointRequestMethods(HttpMethod.GET,HttpMethod.POST,HttpMethod.PUT,HttpMethod.DELETE)
                .tokenServices(authorizationServerTokenServices());//令牌如何生成、如何存储
    }

    @Bean
    public AuthorizationServerTokenServices authorizationServerTokenServices(){
        //构建TokenService对象(此对象提供了创建,获取,刷新token的方法)
        DefaultTokenServices defaultTokenServices = new DefaultTokenServices();

        defaultTokenServices.setTokenEnhancer(jwtAccessTokenConverter);
        defaultTokenServices.setTokenStore(jwtTokenStore);
        defaultTokenServices.setSupportRefreshToken(true);
        defaultTokenServices.setAccessTokenValiditySeconds(3600);
        defaultTokenServices.setRefreshTokenValiditySeconds(3600*24);
        return defaultTokenServices;
    }
}

二 资源服务业务层

1、资源配置类

1.1 令牌配置

此处令牌配置代码与认证授权业务层的代码相同,拷贝过来即可,代码如下:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;

@Configuration
public class TokenConfig {
    private static final String SIGNING_KEY="AUTH";
    @Bean
    public TokenStore jwtTokenStore(){
        return new JwtTokenStore(jwtAccessTokenConverter());
    }
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter(){
        JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
        jwtAccessTokenConverter.setSigningKey(SIGNING_KEY);
        return jwtAccessTokenConverter;
    }
}

1.2 资源服务配置

1.2.1 构建资源服务配置对象

①:添加 @EnableResourceServer 注解,开启服务

②:添加 @EnableGlobalMethodSecurity(prePostEnabled = true) 注解,此注解表示启动方法上的权限控制,需要授权才可访问的方法上添加@PreAuthorize等相关注解。

③:继承ResourceServerConfigurerAdapter类,resource和http方法

@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

1.2.2 重写resource配置方法

通过tokenStore获取token解析器对象,基于此对象对token进行解析。

    @Autowired
    private TokenStore tokenStore;
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        //定义令牌生成策略,不要创建令牌,是要解析令牌
        resources.tokenStore(tokenStore);
    }

1.2.3 重写http配置方法

配置释放相关请求。此处没有进行黑白名单路径配置,可根据需求在自行添加其他配置。

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().anyRequest().permitAll();
        //认证异常
        http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint());
        //访问异常
        http.exceptionHandling().accessDeniedHandler(accessDeniedHandler());
    }

1.2.4 自定义异常处理(http配置方法异常处理需要此配置)

1、构建Json转换方法(此方法可以定义成一个工具类后期调用,这里直接写在了配置类里)。

    private void writeJsonToClient(HttpServletResponse response,
                                   Map map) throws IOException {
        String jsonStr = new ObjectMapper().writeValueAsString(map);
        response.setCharacterEncoding("utf-8");
        response.setContentType("application/json;charset=utf-8");
        PrintWriter writer = response.getWriter();
        writer.println(jsonStr);
        writer.flush();
    }

2、认证异常处理

    @Bean
    public AuthenticationEntryPoint authenticationEntryPoint(){
        return (request, response, exception) -> {
            HashMap map = new HashMap<>();
            map.put("state", HttpServletResponse.SC_UNAUTHORIZED);
            map.put("message","没有认证~~~");
            writeJsonToClient(response,map);
        };
    }

3、访问异常处理

    @Bean
    public AccessDeniedHandler accessDeniedHandler() {
        return (request, response, exception) -> {
            HashMap map = new HashMap<>();
            map.put("state", HttpServletResponse.SC_FORBIDDEN);
            map.put("message", "没有访问权限,请联系管理员");
            writeJsonToClient(response,map);
        };
    }

2.5 完整参考代码

import org.codehaus.jackson.map.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;

@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
    @Autowired
    private TokenStore tokenStore;
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        //定义令牌生成策略,不要创建令牌,是要解析令牌
        resources.tokenStore(tokenStore);
    }
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().anyRequest().permitAll();
        http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint());
        http.exceptionHandling().accessDeniedHandler(accessDeniedHandler());
    }
    @Bean
    public AuthenticationEntryPoint authenticationEntryPoint(){
        return (request, response, exception) -> {
            HashMap map = new HashMap<>();
            map.put("state", HttpServletResponse.SC_UNAUTHORIZED);
            map.put("message","没有认证~~~");
            writeJsonToClient(response,map);
        };
    }
    @Bean
    public AccessDeniedHandler accessDeniedHandler() {
        return (request, response, exception) -> {
            HashMap map = new HashMap<>();
            map.put("state", HttpServletResponse.SC_FORBIDDEN);
            map.put("message", "没有访问权限,请联系管理员");
            writeJsonToClient(response,map);
        };
    }
    private void writeJsonToClient(HttpServletResponse response,
                                   Map map) throws IOException {
        String jsonStr = new ObjectMapper().writeValueAsString(map);
        response.setCharacterEncoding("utf-8");
        response.setContentType("application/json;charset=utf-8");
        PrintWriter writer = response.getWriter();
        writer.println(jsonStr);
        writer.flush();
    }
}

2、授权业务处理

在Controller控制层方法上添加 @PreAuthorize 注解,并使用 hasAuthority() 方法,传入授权信息。

*注:在资源配置类上 添加 @EnableGlobalMethodSecurity(prePostEnabled = true) 注解,@PreAuthorize 注解才会生效。

参考代码

    @PreAuthorize("hasAuthority('sys:user:status')")
    @PutMapping("/updateStatus")
    public SysResult updateStatus(@RequestBody User user){
        userService.updateStatus(user);
        return SysResult.success();
    }

你可能感兴趣的:(java,spring,idea)