spring-cloud-starter-security和spring-cloud-starter-oauth2

张亦
2023-12-01

spring-cloud-starter-security和spring-cloud-starter-oauth2

之前学过spring-security,最近又在学习spring-cloud-starter-security和spring-cloud-starter-oauth2, 脑子里顿时冒出一个问题: 之前学的spring-security和最近学的spring-cloud-starter-security有什么关系, spring-cloud-starter-security和spring-cloud-starter-oauth2又是什么关系, 什么情况下使用哪一个?本来之前的spring-security已经搞懂了,突然冒出来spring-cloud-starter-security和spring-cloud-starter-oauth2, 联想起思考把我前面的理解推翻了,于是又重新整理,可谓一叶障目,不见泰山. 自己学习资料的代码把我整得头晕, 我又上网查找资料, 看别人的博客, 在这里我只写spring-cloud-starter-security和spring-cloud-starter-oauth2的关系, 主要是总结下学到的知识, 也便日后参考, 文字简陋, 如果有人读到这篇文章有读不懂的地方可以多找点资料读, 看看大家怎么说, 然后返回读, 也欢迎提问, 共同进步
首先我总结一下(个人观点):
spring-cloud-starter-security主要用来认证和授权, 怎么授权呢? spring-cloud-starter-security提供许多拦截器, 当你在登录的时候, 他就把你拦下来检查,看有没有登录信息, 如果没有就不给过, 在requestMapping上, 由于加了需要某某权限才能访问此方法, 于是又被拦下检查, 这次检查有没有需要的权限
spring-cloud-starter-oauth2干涉么的呢? 它用主要来认证, 它只负责发放令牌, 你只要带着它要求的参数, 通过它给的路径去访问它, 他就给你一个令牌, 别的事他不管, 这岂不是太简单了, 我怎么不能自己写一个来发放令牌, 用它的还得学, 多麻烦, 你当然可以自己写, 它只是提供一个框架而已, 但是你自己写的话不规范, 人家可是按照oauth2协议写的, oauth2协议参考文档:https://tools.ietf.org/html/rfc6749
并且人家还提供各种加密算法, 防止CSRP攻击等
每个人都会问, 那么该怎么写代码? 下面开始讲一些常用, 它提供的东西肯定很多学不完, 只要抓住关键的, 半部论语治天下即可

一, spring-cloud-starter-security

使用spring-cloud-starter-security需要导入坐标:

		<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-security</artifactId>
        </dependency>

当然spring-cloud-starter-security和spring-cloud-starter-oauth2可以写在一起, 但我分开写

接下来分三步, 一是配置拦截策略(继承WebSecurityConfigurerAdapter类), 二是controller上加注解@PreAuthorize,三是实现UserDetailsService 接口(查数据库获取用户信息)

. 首先,我们设置拦截策略, 哪些资源不拦截等, 需要继承spring-security-config.jar包(这个jar包被spring-cloud-starter-security传递依赖, 可在idea依赖中查看,层级很深)提供的配置类WebSecurityConfigurerAdapter , 重写方法, 并通过@EnableWebSecurity告知这个方法被重写了


import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
@EnableWebSecurity
@Order(-1)
@EnableGlobalMethodSecurity(prePostEnabled=true):
class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    public void configure(WebSecurity web) throws Exception {
    	//忽略哪些资源不用security来管理
        web.ignoring().antMatchers("/userlogin","/userlogout","/userjwt");

    }
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        //直接使用它默认的manager
        AuthenticationManager manager = super.authenticationManagerBean();
        return manager;
    }
    //采用bcrypt对密码进行编码,也可以new 一个MD5加密, 看需要
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    @Override
    public void configure(HttpSecurity http) throws Exception {
    	//配置策略,比如防止csrf攻击,根据需求,不配就使用默认的也行
        http.csrf().disable()
                .httpBasic().and()
                .formLogin()
                .and()
                .authorizeRequests().anyRequest().authenticated();

    }
}

策略写好了, 任何访问都会被拦下来(除了上面策略允许的), controller我们怎么写呢?
.控制器类加@PreAuthorize注解, 在需要被权限管理的方法上这样写:
(2020/6/5日补充: 要使@PreAuthorize(“hasAuthority(‘course_teachplan_add’)”)生效, 必须在WebSecurityConfig配置类上加@EnableGlobalMethodSecurity(prePostEnabled=true)

    @RestController
    @RequestMapping("/course")
    public  class TController{
    
    @PreAuthorize("hasAuthority('course_teachplan_add')")
    @PostMapping("/teachplan/add")
    public ResponseResult addTeachplan(@RequestBody  Teachplan teachplan) {
        return courseService.addTeachplan(teachplan);
    }
    }

表示需要"course_teachplan_add"权限才能访问(这些表示权限含义的字符串随你定义) ,学过注解的都知道当扫描到@PreAuthorize(“hasAuthority(‘course_teachplan_add’)”) 这个注解时, 框架肯定会去调用相关的类或方法来检查请求访问的用户有没有权限
三. 实现UserDetailsService接口, spring会调用多态方式调用UserDetailsService这(注解会调用方法把你写的类加载到容器里,把他的默认的处理类覆盖掉)个接口的实现方法: UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException; 返回值是用户详细信息, 里面有用户名,密码,和权限数组, 因此你可以实现UserDetailsService这个类,重写loadUserByUsername(String var1)这个方法, 在方法里去查数据库把UserDetails 信息返回给他, 匹不匹配这些判断就交给spring去做了
栗子代码如下:


@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    UserClient userClient;
    
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    	//此栗子已在微服务写好查询数据库方法,直接远程调用
       UserDetails userDetails= userClient.getUserext(username);
        return userDetails;
    }
}

这三步就可以配置权限管理业务了, 一般开发绰绰有余, 当然你想更改框架的默认的东西也可以, 你只要继承spring的类或者接口,重写他的方法,写好后提前把它加载到内存中即可,等spring要加载相关类时,发现容器已经有了,就不再加载了,不然让它自己加载很可能找不到我们写的类,我在这里总结下另外一篇文章说的(主要使文章充盈)并附上链接

1.我们可以实现这个接口:FilterInvocationSecurityMetadataSource
2. 实现AccessDecisionManager接口
3.实现AccessDeniedHandler接口
4.继承WebSecurityConfigurerAdapter类(文章开头我们就继承这个类来修改配置的)
我相信不止这些…
这些类和接口用来干嘛呢? 下面分别讲一讲:

(1)

FilterInvocationSecurityMetadataSource

FilterInvocationSecurityMetadataSource接口有三个方法,其中一个主要功能是 封装你在controller层方法上写的权限集为一个集合, 封装干嘛呢? 封装的结果肯定有方法来调用,和前面查询数据库封装的UserDetails比较, 当然读取权限集需要相应的方法, spring里有肯定有, 比如ResourceService类提供了许多方法如getResourceByUrl(requestUrl)读取路径上权限控制信息(资源), 详细请看原文,有例子

(2)

AccessDecisionManager

字面意思: 访问决策管理器
在此接口实现类中可以自己决定满足什么样的条件才允许访问目标资源,详细请看原文,有例子

(3)

AccessDeniedHandler

字面意思: 访问拒绝处理器
在此接口实现类中可以自己决定被拒绝之后做些什么,怎么处理等,详细请看原文,有例子

(4)

WebSecurityConfigurerAdapter

字面意思: Web安全配置器适配器
在此类中可以自己定义web安全配置,http安全配置等,详细请看原文,有例子

读到这里,如果你看了原文, 不知道你有没有注意到他的例子, spring Component包中的类要加载到容器使用的
@Component注解, 而config包中的类使用的@Configuration @EnableWebSecurity注解

二,spring-cloud-starter-oauth2

spring-cloud-starter-oauth2使用需要导入坐标:

 		<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>

. 首先,我们继承AuthorizationServerConfigurerAdapter(认证服务配置适配器)类,看接口源码:

public class AuthorizationServerConfigurerAdapter implements AuthorizationServerConfigurer {
       public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
       }

       public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
       }

       public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
       }
}
我们需要重写这个适配器类的这三个方法(不写就是用默认的, 根据需求,一般需要重写),你肯定好奇这里面要配置什么?

看栗子(三个方法已经用分割线隔开,第一个分割线的内容不是重写方法,看完栗子后解释):


@Configuration
@EnableAuthorizationServer
class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    /*>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>*/
    // //读取密钥的配置,密钥文件已经放在类路径下,密钥怎么生成自行百度,并注入容器(加载到spring管理的内存)
    @Bean("keyProp")
    public KeyProperties keyProperties(){
        return new KeyProperties();
    }
    //引用上一步注入的keyProp,并注入容器,下一步用
    @Resource(name = "keyProp")
    private KeyProperties keyProperties;
    //配置密钥转换器,注入容器(加载到spring管理的内存),下一步使用
    @Bean
    //CustomUserAuthenticationConverter是我们重写的类,继承自DefaultUserAuthenticationConverter,见另一个配置
    public JwtAccessTokenConverter jwtAccessTokenConverter(CustomUserAuthenticationConverter customUserAuthenticationConverter) {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        KeyPair keyPair = new KeyStoreKeyFactory(keyProperties.getKeyStore()
                .getLocation(), keyProperties.getKeyStore().getSecret().toCharArray())
                .getKeyPair(keyProperties.getKeyStore().getAlias(),keyProperties.getKeyStore().getPassword().toCharArray());
        converter.setKeyPair(keyPair);
        //配置自定义的CustomUserAuthenticationConverter
        DefaultAccessTokenConverter accessTokenConverter = (DefaultAccessTokenConverter) converter.getAccessTokenConverter();
        accessTokenConverter.setUserTokenConverter(customUserAuthenticationConverter);
        return converter;
    }
    //引用上一步的密钥转换器
    @Bean
    @Autowired
    public TokenStore tokenStore(JwtAccessTokenConverter jwtAccessTokenConverter) {
        return new JwtTokenStore(jwtAccessTokenConverter);
    }
    /*<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<*/

    /*********************************************************************/

    /*>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>*/
    //客户端配置
    @Autowired
    //spring boot 自动配置dataSource,这里引用就可以了,前提是yml配置了用户名密码等
    private DataSource dataSource;
    @Bean
    //ClientDetailsService由spring提供,肯定可以继承它而改写它的方法
    public ClientDetailsService clientDetails() {
        return new JdbcClientDetailsService(dataSource);
    }
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.jdbc(this.dataSource).clients(this.clientDetails());
    }
    /*<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<*/

    /*********************************************************************/

    /*>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>*/
    //授权服务器端点配置,让授权服务器按我们需求生成令牌,比如加入或删除某些信息

    //jwt令牌转换器
    @Autowired
    //JwtAccessTokenConverter由spring提供,肯定也可以继承它而改写它的方法
    private JwtAccessTokenConverter jwtAccessTokenConverter;
    @Autowired
    //spring-cloud-starter-oauth2允许令牌生成时加入用户详情信息,这里想加用户信息,
            // 所以把在spring-cloud-starter-security里重写的UserDetailsService引入了
    UserDetailsService userDetailsService;
    @Autowired
    AuthenticationManager authenticationManager;
    @Autowired
    TokenStore tokenStore;
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.accessTokenConverter(jwtAccessTokenConverter)
                .authenticationManager(authenticationManager)//认证管理器
                .tokenStore(tokenStore)//令牌存储
                .userDetailsService(userDetailsService);//用户信息service
    }
    /*<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<*/

    /*********************************************************************/

    /*>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>*/
    //授权服务器的安全配置
    @Override
    public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
        // oauthServer.checkTokenAccess("isAuthenticated()");//校验token需要认证通过,可采用http basic认证
        oauthServer.allowFormAuthenticationForClients()
                .passwordEncoder(new BCryptPasswordEncoder())
                .tokenKeyAccess("permitAll()")
                .checkTokenAccess("isAuthenticated()");
    }
    /*<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<*/


}

解释

第一段分割线内容不是对方法的重写,但是spring oauth2肯定会调用这些方法或类,那我们在它方法或类里做些手脚(调用加密算法把生成令牌再加密,当然spring oauth2提供了加密算法),并提前把类加载,至于具体使用时机没必要那么透彻,我们可以肯定的是,spring会主动加载这些类,只不过我们提前把这些类加载了,你可能会有疑问,spring后面不知道又加载相同的类怎么办,如果你了解java虚拟机类加载器的双亲委托机制就会明白了,不知道自行百度
现在解决第一个分隔符内提到了的CustomUserAuthenticationConverter类, 此类继承spring的DefaultUserAuthenticationConverter,这里我们只改下他的其中一个方法(为了在生成令牌时加入我们想要的用户权限信息,看业务需求),如下

@Component
public class CustomUserAuthenticationConverter extends DefaultUserAuthenticationConverter {
    @Autowired
    //这里又用到了UserDetailsService 
    UserDetailsService userDetailsService;

    @Override
    public Map<String, ?> convertUserAuthentication(Authentication authentication) {
        LinkedHashMap response = new LinkedHashMap();
        String name = authentication.getName();
        response.put("user_name", name);

        Object principal = authentication.getPrincipal();
        UserJwt userJwt = null;
        if(principal instanceof  UserJwt){
            userJwt = (UserJwt) principal;
        }else{
            //refresh_token默认不去调用userdetailService获取用户信息,这里我们手动去调用,得到 UserJwt
            //查询数据库,获取用户信息
            UserDetails userDetails = userDetailsService.loadUserByUsername(name);
            //UserJwt是我自定义的,继承UserDetails
            userJwt = (UserJwt) userDetails;
        }
        //把用户信息加入令牌
        response.put("name", userJwt.getName());
        response.put("id", userJwt.getId());
        response.put("utype",userJwt.getUtype());
        response.put("userpic",userJwt.getUserpic());
        response.put("companyId",userJwt.getCompanyId());
        if (authentication.getAuthorities() != null && !authentication.getAuthorities().isEmpty()) {
            response.put("authorities", AuthorityUtils.authorityListToSet(authentication.getAuthorities()));
        }

        return response;
    }


}

涉及到数据库和密钥, yml配置如下:

  datasource:
    druid:
      url: ${MYSQL_URL:jdbc:mysql://localhost:3306/db_xxx?characterEncoding=utf-8}
      username: root
      password: mysql
      driverClassName: com.mysql.jdbc.Driver
      initialSize: 5  #初始建立连接数量
      minIdle: 5  #最小连接数量
      maxActive: 20 #最大连接数量
      maxWait: 10000  #获取连接最大等待时间,毫秒
      testOnBorrow: true #申请连接时检测连接是否有效
      testOnReturn: false #归还连接时检测连接是否有效
      timeBetweenEvictionRunsMillis: 60000 #配置间隔检测连接是否有效的时间(单位是毫秒)
      minEvictableIdleTimeMillis: 300000  #连接在连接池的最小生存时间(毫秒)
encrypt:
  key-store:
    location: classpath:/xxx.keystore	#密钥在类路径位置
    secret: xxx
    alias: xxx
    password: xxx

spring-cloud-starter-oauth2会读取数据库客户端id和密码,判断该客户端应用在数据库存在且密码匹配才有资格申请令牌,按他的要求建三张表,规则自行百度
配置完成
怎么申请令牌呢? spring要求通过http请求,并携带相应参数,看栗子就明白了

 private AuthToken applyToken(String username, String password, String clientId, String clientSecret){
       
        //令牌申请的地址 http://localhost:40400/auth/oauth/token
        String authUrl = "http://localhost:40400/auth/oauth/token";
        //定义header,spring要求请求头含有客户端id和客户端密码,表示哪个应用来申请令牌,并且要用BASIC64编码,工具他都全部提供好了,直接用
        LinkedMultiValueMap<String, String> header = new LinkedMultiValueMap<>();
        //getHttpBasic()方法自定义在代码最后
        String httpBasic = getHttpBasic(clientId, clientSecret);
        header.add("Authorization",httpBasic);

        //定义body,给谁申请令牌,申请方式等信息写在请求体
        LinkedMultiValueMap<String, String> body = new LinkedMultiValueMap<>();
        body.add("grant_type","password");
        body.add("username",username);
        body.add("password",password);
        //将请求头和请求体封装在一起,spring也提供了工具,在http包中 
        HttpEntity<MultiValueMap<String, String>> httpEntity = new HttpEntity<>(body, header);
        //String url, HttpMethod method, @Nullable HttpEntity<?> requestEntity, Class<T> responseType, Object... uriVariables

        //设置restTemplate远程调用时候,对400和401不让报错,正确返回数据
        restTemplate.setErrorHandler(new DefaultResponseErrorHandler(){
            @Override
            public void handleError(ClientHttpResponse response) throws IOException {
                if(response.getRawStatusCode()!=400 && response.getRawStatusCode()!=401){
                    super.handleError(response);
                }
            }
        });
        //使用restTemplate的post请求开始申请
        ResponseEntity<Map> exchange = restTemplate.exchange(authUrl, HttpMethod.POST, httpEntity, Map.class);

        //申请到的令牌信息,我想判断下申请的令牌信息是否不全,信息全我才使用
        Map bodyMap = exchange.getBody();
        //spring返回的令牌包含三个信息,使用map集合返回,三个信息key分别为:jti(JWT identify),refresh_token(刷新令牌:令牌过期,可以拿着这个令牌去,认证中心给颁发新令牌,刷新令牌只能用一次),access_token(JWT令牌:我们要的就是它)
        if(bodyMap == null ||
            bodyMap.get("access_token") == null ||
                bodyMap.get("refresh_token") == null ||
                bodyMap.get("jti") == null){

            //解析spring security返回的错误信息
            if(bodyMap!=null && bodyMap.get("error_description")!=null){
                String error_description = (String) bodyMap.get("error_description");
                if(error_description.indexOf("UserDetailsService returned null")>=0){
               		//抛出自定义异常,ExceptionCast异常类由我自定义
                    ExceptionCast.cast(AuthCode.AUTH_ACCOUNT_NOTEXISTS);
                }else if(error_description.indexOf("坏的凭证")>=0){
                    ExceptionCast.cast(AuthCode.AUTH_CREDENTIAL_ERROR);
                }
            }


            return null;
        }
        //AuthToken 是自定义实体类,我想将令牌重新封装在返回
        AuthToken authToken = new AuthToken();
        authToken.setAccess_token((String) bodyMap.get("jti"));//用户身份令牌
        authToken.setRefresh_token((String) bodyMap.get("refresh_token"));//刷新令牌
        authToken.setJwt_token((String) bodyMap.get("access_token"));//jwt令牌
        return authToken;
    }



    //获取httpbasic的串
    private String getHttpBasic(String clientId,String clientSecret){
        String string = clientId+":"+clientSecret;
        //将串进行base64编码
        byte[] encode = Base64Utils.encode(string.getBytes());
        return "Basic "+new String(encode);
    }

spring-cloud-starter-oauth2只需要配置,申请就行了,我相信,也同spring-cloud-starter-security套路一样,我们可以继承他提供的类和接口,重写方法可以达到我们的目的,不过其他配置使用它默认的就够了
看spring-cloud-starter-oauth2的规则,客户端应用申请令牌带了哪些参数,客户端id和密码,申请方式,用户用户名密码…
客户端id和密码由spring oauth2认证中心去读数据库,回顾配置吧,只要读到信息匹配就发放令牌,他不管用户名和用户密码…
那配置中我们使用了spring-cloud-starter-security的UserDetailService类的方法读取数据库, 干嘛呢? 为了让spring oauth2生成令牌时把用户信息加在令牌里,所以改写了他的方法…
连续坐了五个多小时了,重庆的冬天还蛮冷的,脚僵得厉害,得去吃晚饭了,不想检查了,有时间了再来看哪里有毛病再修改
2019/12/1 20:39

补充点知识:
spring提供的BCryptPasswordEncoder加密类的使用:

BCryptPasswordEncoder bEncoder=new BCryptPasswordEncoder();
String encodePassword=bEncoder.encode(password);
 类似资料: