欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Spring Cloud:认证 授权 OAuth2、JWT

程序员文章站 2022-06-13 15:46:10
...

OAuth2

OAuth2是当前授权的行业标准,其重点在于为Web应用程序、桌面应用程序、移动设备以及室内设备的授权流程提供简单的客户端开发方式。它为第三方应用提供对HTTP服务的有限访问,既可以是资源拥有者通过授权允许第三方应用获取HTTP服务,也可以是第三方以自己的名义获取访问权限。

角色

OAuth2中主要分为了4种角色:

  • Resource Owner(资源所有者),是能够对受保护的资源授予访问权限的实体,可以是一个用户,这时会称为终端用户(end-user)。
  • Resource Server(资源服务器),持有受保护的资源,允许持有访问令牌(Access Token)的请求访问受保护资源。
  • Client(客户端),持有资源所有者的授权,代表资源所有者对受保护资源进行访问。
  • Authorization Server(授权服务器),对资源所有者的授权进行认证,成功后向客户端发送访问令牌。

很多时候,资源服务器和授权服务器是合二为一的,在授权交互的时候作为授权服务器,在请求资源交互时作为资源服务器

Resource Server的配置

Resource Server(可以是授权服务器,也可以是其他的资源服务)提供了受OAuth2保护的资源,这些资源为API接口、Html页面、Js文件等.Spring OAuth2提供了实现此保护功能的Spring Security认证过滤器。在加了@Configuration注解的配置类上加@EnableResourceServer注解,开启Resource Server的功能

JWT

JSON Web Token(JWT)是一种开放的标准(RFC 7519),JWT定义了一种紧凑且自包含的标准,该标准旨在将各个主体的信息包装为JSON对象。主体信息是通过数字签名进行加密和验证的。常使用HMAC算法或RSA(公钥/私钥的非对称性加密)算法对JWT进行签名,安全性很高。

  • 紧凑性(compact):由于是加密后的字符串,JWT数据体积非常小,可通过POST请求参数或HTTP请求头发送。另外,数据体积小意味着传输速度很快。
  • 自包含(self-contained):JWT包含了主体的所有信息,所以避免了每个请求都需要向Uaa服务验证身份,降低了服务器的负载。

JWT由3个部分组成,分别以“.”分隔,组成部分如下。

  • Header(头):Header通常由两部分组成:令牌的类型(即JWT)和使用的算法类型,如HMAC、SHA256和RSA
  • Payload(有效载荷):了用户的一些信息和Claim(声明、权利)。有3种类型的Claim:保留、公开和私人
  • Signature(签名):需要将Base64编码后的Header、Payload和**进行签名

uaa配置

1. 依赖:

    implementation 'org.springframework.boot:spring-boot-starter-actuator'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.1.2'
    implementation 'org.springframework.cloud:spring-cloud-starter-oauth2:2.2.2.RELEASE'
    implementation 'org.springframework.cloud:spring-cloud-starter-consul-discovery:2.2.2.RELEASE'
    runtimeOnly 'mysql:mysql-connector-java'

2. 配置文件:

bootstrap.yml:

spring:
  application:
    name: consul-auth
  cloud:
    consul:
      host: localhost
      port: 8500
      discovery:
        prefer-ip-address: true
        service-name: ${spring.application.name}


management:
  endpoints:
    web:
      exposure:
        include: "*"
  endpoint:
    health:
      show-details: always

application.yml:

server:
  port: 8731

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/ffzs?useUnicode=true&zeroDateTimeBehavior=convertToNull&autoReconnect=true&characterEncoding=utf-8
    username: root
    password: 123zxc

  main:
    allow-bean-definition-overriding: true

3. 安装mysql数据库

需要通过数据库进行对人员登录人员数据进行存储这里使用了Mysql数据库

使用docker安装:

    mysql:
      image: mysql
      networks:
        - spring
      restart: always
      ports:
        - 33060:33060
        - 3306:3306
      volumes:
        - ./mysql/db:/var/lib/mysql
        - ./mysql/conf.d:/etc/mysql/conf.d
      environment:
        - MYSQL_ROOT_PASSWORD=123zxc    
      command: --default-authentication-plugin=mysql_native_password

创建一个user的表用来存储人员数据:

CREATE TABLE `myUser` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `password` varchar(255) DEFAULT NULL,
  `username` varchar(255) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `name` (`username`)
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8;

BEGIN;
INSERT INTO `myUser` VALUES ('1', '$2a$10$uuFQKbr2q/8aqYlPEBlRw.Z9UrtEPrydIh7IUXaEGVWBowY8mZrUq', 'ffzs'),('2', '$2a$10$QgQ9OtiCMnGzYGPabDzOkeBda0Sb8wqzwnTSErJWPx4GfeNOUvh7q', 'sleepycat');
COMMIT;

添加两个用户

4. AuthorizationServer配置

/**
 * @author ffzs
 * @describe
 * @date 2020/6/7
 */

@Configuration
@EnableAuthorizationServer
public class OAuthConfiguration extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private UserDetailsServiceImpl userServiceDetail;

    /**
     * 配置客户端信息
     * @param clients
     * @throws Exception
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {

        clients.inMemory()
                .withClient("consul_server")  // 用户端id 需要在Authorization Server中是唯一的。
                .secret("123456") // 连接密码
                .scopes("server")  // 配置的客户端域为 service
//                .autoApprove(true)   // client_secret
//                .authorities("ROLE_ADMIN", "ROLE_USER")  // 权限信息
                .authorizedGrantTypes("implicit", "refresh_token", "password", "authorization_code", "client_credentials")  // 类验证类型
                .accessTokenValiditySeconds(60*60);  //失效时间
    }

    /**
     * 配置授权Token的节点和Token服务
     * @param endpoints
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints
                .authenticationManager(authenticationManager) //只有配置了该选项,密码认证才会开启。在大多数情况下都是密码验证,所以一般都会配置这个选项
                /**
                 * 需要设置Token的管理策略,目前支持以下3种:
                 * InMemoryTokenStore:Token存储在内存中。
                 * JdbcTokenStore:Token存储在数据库中。需要引入spring-jdbc的依赖包,并配置数据源,以及初始化Spring OAuth2的数据库脚本
                 * JwtTokenStore:采用JWT形式,这种形式没有做任何的存储,因为JWT本身包含了用户验证的所有信息,不需要存储。采用这种形式,需要引入spring-jwt的依赖
                 */
                .tokenStore(jwtTokenStore())
                .tokenEnhancer(jwtTokenEnhancer())
//                .userDetailsService(userServiceDetail)  // 配置获取用户认证信息的接口
                .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST);
    }

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

    @Bean
    protected JwtAccessTokenConverter jwtTokenEnhancer() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey("fanfanzhisu");
        return converter;
    }

    /**
     * Token 节点的安全策略
     * @param oauthServer
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
        oauthServer
                .tokenKeyAccess("permitAll()")
                .checkTokenAccess("isAuthenticated()")
                .allowFormAuthenticationForClients()
                .passwordEncoder(NoOpPasswordEncoder.getInstance());
    }
}

5. WebSecurity 配置

  • 开放了actuator路径供健康检查
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsServiceImpl userServiceDetail;

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

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                .exceptionHandling()
                .authenticationEntryPoint((request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED))
                .and()
                .authorizeRequests()
                .antMatchers("/actuator/**").permitAll()
                .antMatchers("/**").authenticated()
                .and()
                .httpBasic();
    }
//
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
                .userDetailsService(userServiceDetail)
                .passwordEncoder(new BCryptPasswordEncoder());
    }


    @Bean
    public static NoOpPasswordEncoder passwordEncoder() {
        return (NoOpPasswordEncoder) NoOpPasswordEncoder.getInstance();
    }
}

Mybatis文件配置:

配置一下model,dao等文件路径:

@Configuration
@MapperScan("com.ffzs.consulauth.**.dao")
public class MybatisConfig {

    @Autowired
    private DataSource dataSource;

    @Bean
    public SqlSessionFactory sqlSessionFactory() throws Exception {
        SqlSessionFactoryBean ssfb = new SqlSessionFactoryBean();
        ssfb.setDataSource(dataSource);
        ssfb.setTypeAliasesPackage("com.ffzs.consulauth.**.model");

        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        ssfb.setMapperLocations(resolver.getResources("classpath*:**/generator/*.xml"));

        return ssfb.getObject();
    }
}

model,dao等文件通过idea上的generator插件生成即可。
Spring Cloud:认证 授权 OAuth2、JWT

重新写一下获取token时匹配用户部分逻辑

UserDetailsServiceImpl.class

  • 管理员给ROLE_ADMIN, ROLE_USER 权限
  • 普通用户只给 ROLE_USER 权限
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired(required = false)
    private MyUserDao myUserDao;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        MyUser myUser = myUserDao.findByUsername(username);
        System.out.println(username);
        if (myUser == null) {
            throw new UsernameNotFoundException("用户不存在");
        }
        if (username.equals("admin") || username.equals("ffzs")){
            return new User(username, myUser.getPassword(), AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_ADMIN, ROLE_USER"));
        }
        return new User(username, myUser.getPassword(), AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER"));
    }
}

userdao要添加一个通过名字获取信息的方法:

Spring Cloud:认证 授权 OAuth2、JWT

Spring Cloud:认证 授权 OAuth2、JWT

启动类更改:

添加aaa@qq.com==即可

Spring Cloud:认证 授权 OAuth2、JWT

运行&&测试

consul上注册成功
Spring Cloud:认证 授权 OAuth2、JWT

使用postman发送post请求:

http://localhost:8731/oauth/token?client_id=consul_server&client_secret=123456&grant_type=password&username=ffzs&password=123zxc

成功获取token

Spring Cloud:认证 授权 OAuth2、JWT

post请求中的参数及描述:
Spring Cloud:认证 授权 OAuth2、JWT

service配置

创建一个consul-service项目,用于提供登录,注册等服务:

1. 依赖:

    implementation 'org.springframework.boot:spring-boot-starter-actuator'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.cloud:spring-cloud-starter-openfeign:2.2.2.RELEASE'
    implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.1.2'
    implementation 'org.springframework.cloud:spring-cloud-starter-consul-discovery:2.2.2.RELEASE'
    implementation 'org.springframework.cloud:spring-cloud-starter-oauth2:2.2.2.RELEASE'
    runtimeOnly 'mysql:mysql-connector-java'

2. 配置文件

bootstrap.yml:

spring:
  application:
    name: consul-service
  cloud:
    consul:
      host: localhost
      port: 8500
      discovery:
        prefer-ip-address: true
        service-name: ${spring.application.name}

feign:
  httpclient:
    enabled: true

management:
  endpoints:
    web:
      exposure:
        include: "*"
  endpoint:
    health:
      show-details: always

application.yml:

server:
  port: 8777

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/ffzs?useUnicode=true&zeroDateTimeBehavior=convertToNull&autoReconnect=true&characterEncoding=utf-8
    username: root
    password: 123zxc

logging:
  level:
    root: INFO
    org.springframework.web: INFO
    org.springframework.security: INFO
    org.springframework.security.oauth2: INFO

3. Mybatis配置

dao,model跟uaa的一样

mybatis的配置文件跟uaa的也基本相同:

@Configuration
@MapperScan("com.ffzs.consulservice.**.dao")
public class MybatisConfig {

    @Autowired
    private DataSource dataSource;

    @Bean
    public SqlSessionFactory sqlSessionFactory() throws Exception {
        SqlSessionFactoryBean ssfb = new SqlSessionFactoryBean();
        ssfb.setDataSource(dataSource);
        ssfb.setTypeAliasesPackage("com.ffzs.consulservice.**.model");

        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        ssfb.setMapperLocations(resolver.getResources("classpath*:**/generator/*.xml"));

        return ssfb.getObject();
    }
}

4. Resource配置

ResourceConfiguration.class:

  • 配置了两个测试路径,user路径用于登录,不做权限限制
  • /hello/admin", "/hello/header只右ADMIN权限才能访问
@Configuration
@EnableResourceServer
public class ResourceConfiguration extends ResourceServerConfigurerAdapter {

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                .authorizeRequests()
                .antMatchers("/actuator/**").permitAll()
                .antMatchers("/user/**").permitAll()
                .antMatchers("/hello/user", "hello/test").hasRole("USER")
                .antMatchers("/hello/admin", "/hello/header").hasAnyRole("ADMIN", "USER")
                .antMatchers("/**").authenticated();
    }

    /**
     * tokenServices 定义Token Service 用ResourceServerTokenservices类,配置Token是如何编码和解码的 可以用RemoteTokenServices类,即Resource Server采用远程授权服务器进行Token解码,这时也不需要配置此选项
     *
     * @param resources
     * @throws Exception
     */
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources
                .resourceId("server")  // 配置资源Id。
                .tokenStore(jwtTokenStore());
    }

    @Bean
    protected JwtAccessTokenConverter jwtTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey("fanfanzhisu");
        return converter;
    }

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

5. 注册功能

添加一个功能用于注册,service

    public int insertUser(String username, String  password){
        MyUser user=myUserDao.findByUsername(username);
        if (user != null){
            user.setPassword(encoder.encode(password));
            return myUserDao.updateByPrimaryKeySelective(user);
        }else{
            MyUser myUser=new MyUser();
            myUser.setUsername(username);
            myUser.setPassword(encoder.encode(password));
            return myUserDao.insertSelective(myUser);
        }
    }

controller:

    @PostMapping("/register")
    public String postUser(@RequestParam("username") String username , @RequestParam("password") String password){
        int back = userServiceDetail.insertUser(username,password);
        return back == 1?"注册成功":"注册失败";
    }

6. 登录功能

编写一个service通过feign向注册中心的consul-auth,也就是上面uaa发送登录请求并获取token

@FeignClient(value = "consul-auth")
public interface AuthServiceClient {

    @PostMapping(value = "/oauth/token")
    MyToken getToken(@RequestHeader("Content-Type") String content, @RequestParam("client_id") String client_id, @RequestParam("client_secret") String client_secret, @RequestParam("grant_type") String type,
                     @RequestParam("username") String username, @RequestParam("password") String password);

}

调用getToken获取token,之后装入userLoginDTO中:

    public UserLoginDTO login(String username, String password){
        MyUser user=myUserDao.findByUsername(username);
        if (null == user) {
            throw new RuntimeException("error username");
        }
        if(!encoder.matches(password,user.getPassword())){
            throw new RuntimeException("error password");
        }

        MyToken myToken =authServiceClient.getToken("application/json", "consul_server","123456","password", username, password);

        if(myToken ==null){
            throw new RuntimeException("error internal");
        }

        UserLoginDTO userLoginDTO=new UserLoginDTO();
        userLoginDTO.setMyToken(myToken);
        userLoginDTO.setUser(user);
        return userLoginDTO;
    }

用于登入的controller:

    @PostMapping("/login")
    public UserLoginDTO login(@RequestParam("username") String username , @RequestParam("password") String password){
        return userServiceDetail.login(username,password);
    }

7. 测试登录

测试注册功能,访问http://localhost:8777/user/register?username=xiaozhang&password=123456,结果如下:

Spring Cloud:认证 授权 OAuth2、JWT

查看数据库,xiaozhang的用户信息已经存入数据库:

Spring Cloud:认证 授权 OAuth2、JWT

测试登入功能,http://localhost:8777/user/login?username=xiaozhang&password=123456 ,返回token说明登录成功:

Spring Cloud:认证 授权 OAuth2、JWT

8. 测试权限

因为一些业务的需求会有一些端口需要鉴权,编写用于测试的controller:

@RestController
@RequestMapping("hello")
public class TestController {

    @GetMapping("user")
    public String user(){
        return "hello!!! 普通用户 !!!";
    }

    @GetMapping("admin")
    public String admin(){
        return "hello!!! 权限dog !!!!";
    }

    @GetMapping("role")
    public String test() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        StringBuilder res = new StringBuilder()
                .append("用户名: ").append(authentication.getName()).append("\n")
                .append("权限情况: ");
        for (Object it :authentication.getAuthorities().toArray()) {
            res.append(it).append("\t");
        }
        return res.toString();
    }

    @RequestMapping("header")
    public String header(HttpServletRequest request) {
        StringBuilder html = new StringBuilder("<table border='2' cellspacing='0'>");
        Enumeration<String> headerNames = request.getHeaderNames();
        while (headerNames.hasMoreElements()) {
            String key = headerNames.nextElement();
            html.append("<tr><td>").append(key).append("</td><td>").append(request.getHeader(key)).append("</td></tr>");
        }
        html.append("</table>");
        return html.toString();
    }
}

几个端口权限设置如下:

                .antMatchers("/hello/user", "hello/test").hasRole("USER")
                .antMatchers("/hello/admin", "/hello/header").hasAnyRole("ADMIN", "USER")
  • USER 权限可以访问 “/hello/user”, “hello/test”
  • ADMIN 都可以访问

设置token,使用postman可以直接设置,如下图,不使用postman的话可以写道header里:

Spring Cloud:认证 授权 OAuth2、JWT

这时我们访问,http://localhost:8777/hello/user,可以访问user;

Spring Cloud:认证 授权 OAuth2、JWT

访问,http://localhost:8777/hello/admin,因为没有权限,限制访问:

Spring Cloud:认证 授权 OAuth2、JWT

访问,http://localhost:8777/hello/role,这里我们可以看到xiaozhang只有ROLU_USER的权限:

Spring Cloud:认证 授权 OAuth2、JWT

更换用户,使用ffzs账号登录,更换获得token,权限都token都长了:

Spring Cloud:认证 授权 OAuth2、JWT

使用ffzs的账号访问http://localhost:8777/hello/role

Spring Cloud:认证 授权 OAuth2、JWT

ROLE_ADMIN ROLE_USER同时拥有两个权限:

这时我们试试用这个账号能否访问http://localhost:8777/hello/admin

Spring Cloud:认证 授权 OAuth2、JWT

有了ADMIN权限可以正常访问了。

看一下header,访问 http://localhost:8777/hello/header

Spring Cloud:认证 授权 OAuth2、JWT

可见token在header里的形式,如果不容postman发送请求就添加通过将"authorization":"Bearer token"添加到你的header中就可以访问了