SpringSecurityOauth2+JWT实现单点登录

单点登录的介绍

单点登录(Single Sign On),简称为 SSO,SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。

OAuth2 相关解释

  • Resource owner(资源拥有者):拥有该资源的最终用户,他有访问资源的账号密码;
  • Resource server(资源服务器):拥有受保护资源的服务器,如果请求包含正确的访问令牌,可以访问资源;
  • Client(客户端):访问资源的客户端,会使用访问令牌去获取资源服务器的资源,可以是浏览器、移动设备或者服务器;
  • Authorization server(认证服务器):用于认证用户的服务器,如果客户端认证通过,发放访问资源服务器的令牌。

四种授权模式

  • Authorization Code(授权码模式):客户端先将用户导向认证服务器,登录后获取授权码,然后进行授权,最后根据授权码获取访问令牌;
  • Implicit(简化模式):和授权码模式相比,取消了获取授权码的过程,直接获取访问令牌;
  • Resource Owner Password Credentials(密码模式):客户端直接向用户获取用户名和密码,之后向认证服务器获取访问令牌;
  • Client Credentials(客户端模式):客户端直接通过客户端认证(比如client_id和client_secret)从认证服务器获取访问令牌。

首先创建oauth2-server模块

我们先创建一个oauth2-server服务作为登录的服务端模块,Spring Cloud Security中有两种存储令牌的方式,一种是使用Redis来存储,另一种是使用JWT来存储,这里我们采用了JWT来存储令牌。

先在pom.xml引入相关依赖

		<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.0</version>
        </dependency>

在application.yml中进行配置相关文件

server:
  port: 9001
spring:
  application:
    name: oauth2-server

添加UserService实现UserDetailsService接口,用于加载用户信息

@Service
public class UserService implements UserDetailsService {
    private List<User> userList;
    @Autowired
    private PasswordEncoder passwordEncoder;

    @PostConstruct
    public void init() {
        String password = passwordEncoder.encode("123");
        userList = new ArrayList<>();
        userList.add(new User("admin", password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin")));
        userList.add(new User("user", password, AuthorityUtils.commaSeparatedStringToAuthorityList("client")));
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        List<User> users = userList.stream().filter(user -> user.getUsername().equals(username)).collect(Collectors.toList());
        if (!CollectionUtils.isEmpty(users)) {
            return users.get(0);
        } else {
            throw new UsernameNotFoundException("用户名或密码错误");
        }
    }
}

添加JWT存储令牌的配置:

@Configuration
public class JwtTokenStoreConfig {

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

    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter();
        accessTokenConverter.setSigningKey("jwttest_key");//配置JWT使用的秘钥
        return accessTokenConverter;
    }
}

添加认证服务器配置,使用@EnableAuthorizationServer注解开启配置认证服务器

Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private UserService userService;

    @Autowired
    private TokenStore tokenStore;
    @Autowired
    private JwtAccessTokenConverter jwtAccessTokenConverter;
    @Autowired
    private JwtTokenEnhancer jwtTokenEnhancer;

    /**
     * 使用密码模式需要配置
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
        List<TokenEnhancer> delegates = new ArrayList<>();
        delegates.add(jwtAccessTokenConverter);
        enhancerChain.setTokenEnhancers(delegates);
        endpoints.authenticationManager(authenticationManager)
                .userDetailsService(userService)
                .tokenStore(tokenStore) //配置令牌存储策略
                .accessTokenConverter(jwtAccessTokenConverter)
                .tokenEnhancer(enhancerChain);
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                .withClient("root")
                .secret(passwordEncoder.encode("123456"))//这里密码需要进行加密
                .accessTokenValiditySeconds(3600)
                .refreshTokenValiditySeconds(10800) //设置刷新令牌失效时间
                .redirectUris("http://localhost:9002/login") //单点登录时配置,访问客户端需要授权的接口,会跳转到该路径
                .autoApprove(true) //自动授权配置
                .scopes("all")
                .authorizedGrantTypes("authorization_code","password","refresh_token");
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) {
        security.tokenKeyAccess("isAuthenticated()"); // 获取密钥需要身份认证,使用单点登录时必须配置
    }
}

添加资源服务器配置,使用@EnableResourceServer注解开启配置资源服务器

@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest()
                .authenticated()  
                .and()
                .requestMatchers()
                .antMatchers("/user/**");//表示该安全规则只针对参数指定的路径进行过滤,因为测试没有前端代码,先这样去做
    }
}

添加SpringSecurity配置,允许认证相关路径的访问及表单登录

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

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

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.csrf()
                .disable()
                .authorizeRequests()
                .antMatchers("/oauth/**", "/login/**", "/logout/**")
                .permitAll()
                .anyRequest()
                .authenticated()
                .and()
                .formLogin()
                .permitAll();
    }
}

添加需要登录的接口用于测试,使用jjwt工具类来解析Authorization头中存储的JWT内容

@RestController
@RequestMapping("/user")
public class UserController {
    @GetMapping("/getUser")
    public Object getUser(Authentication authentication, HttpServletRequest request) {
        String header = request.getHeader("Authorization");
        String token = StrUtil.subAfter(header, "bearer ", false);//取分隔字符串之后字符串不包括分隔字符串
        return Jwts.parser()
                .setSigningKey("jwttest_key".getBytes(StandardCharsets.UTF_8))
                .parseClaimsJws(token)
                .getBody();
    }

}

创建oauth2-client模块

这里我们创建一个oauth2-client服务作为需要登录的客户端,当我们在oauth2-server服务上登录以后,就可以直接访问oauth2-client需要登录的接口,来演示下单点登录功能。

在pom.xml中添加相关依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.0</version>
</dependency>

在application.yml中进行配置

server:
  port: 9002
  servlet:
    session:
      cookie:
        name: OAUTH2-CLIENT-SESSIONID #防止Cookie冲突,冲突会导致登录验证不通过
oauth2-server-url: http://localhost:9001
spring:
  application:
    name: oauth2-client
security:
  oauth2: #与oauth2-server对应的配置
    client:
      client-id: root
      client-secret: 123456
      user-authorization-uri: ${oauth2-server-url}/oauth/authorize
      access-token-uri: ${oauth2-server-url}/oauth/token
    resource:
      jwt:
        key-uri: ${oauth2-server-url}/oauth/token_key

在启动类上添加@EnableOAuth2Sso注解来启用单点登录功能

@EnableOAuth2Sso
@SpringBootApplication
public class Oauth2ClientApplication {

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

}

添加测试接口用于获取当前登录用户信息:

@RestController
@RequestMapping("/user")
public class UserController {
    
    @GetMapping("/getUser")
    public Object getUser(Authentication authentication) {
        return authentication;
    }

}

使用网页来演示单点登录

  • 启动oauth2-client服务和oauth2-server服务;

  • 访问客户端需要授权的接口http://localhost:9002/user/getUser会跳转到授权服务的登录界面;

  • 输入账号密码进行登录操作

  • 在这里插入图片描述
    在这里插入图片描述
    发现获取到的令牌已经变成了JWT令牌,将tokenValue拿到https://jwt.io/ 网站上去解析下可以获得其中内容。

{
  "user_name": "admin",
  "scope": [
    "all"
  ],
  "exp": 1587094663,
  "authorities": [
    "admin"
  ],
  "jti": "67a4a31f-021d-42ba-884e-a0270af68e02",
  "client_id": "root"
}

调用接口来演示单点登录

这里我们使用Postman来演示下

  • 访问客户端需要登录的接口:http://localhost:9002/user/getUser

  • 使用Oauth2认证方式获取访问令牌
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    最后请求接口可以获取到相关用户信息

{
    "authorities": [
        {
            "authority": "admin"
        }
    ],
    "details": {
        "remoteAddress": "0:0:0:0:0:0:0:1",
        "sessionId": "EF89129C446E55917A74753E59B5CBF2",
        "tokenValue": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsInNjb3BlIjpbImFsbCJdLCJleHAiOjE1ODcwOTYzODMsImF1dGhvcml0aWVzIjpbImFkbWluIl0sImp0aSI6IjU3ZDk4YTZlLWYyYmEtNGIyMC04OTQzLTgxYjFhMjQ5MGY5MCIsImNsaWVudF9pZCI6InJvb3QiLCJlbmhhbmNlIjoiZW5oYW5jZSBpbmZvIn0.8A43o9pK4RfDQW5qmInOAGyQKE3TVq55iTuCvfTQzo8",
        "tokenType": "bearer",
        "decodedDetails": null
    },
    "authenticated": true,
    "userAuthentication": {
        "authorities": [
            {
                "authority": "admin"
            }
        ],
        "details": null,
        "authenticated": true,
        "principal": "admin",
        "credentials": "N/A",
        "name": "admin"
    },
    "clientOnly": false,
    "oauth2Request": {
        "clientId": "root",
        "scope": [
            "all"
        ],
        "requestParameters": {
            "client_id": "root"
        },
        "resourceIds": [],
        "authorities": [],
        "approved": true,
        "refresh": false,
        "redirectUri": null,
        "responseTypes": [],
        "extensions": {},
        "refreshTokenRequest": null,
        "grantType": null
    },
    "principal": "admin",
    "credentials": "",
    "name": "admin"
}

oauth2-client添加权限校验

添加配置开启基于方法的权限校验:

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Order(999)//将这里设置为最后执行,Order值越小优先级越高
public class SecurityConfig extends WebSecurityConfigurerAdapter {
}

在UserController中添加需要admin权限的接口:

@RestController
@RequestMapping("/user")
public class UserController {

    @PreAuthorize("hasAuthority('admin')")
    @GetMapping("/auth/admin")
    public Object adminAuth() {
        return "你具有访问的权限!";
    }

}

访问接口:http://localhost:9002/user/auth/admin

使用admin权限的帐号的账号访问 ,账号admin 密码 123
在这里插入图片描述
使用没有admin权限的帐号的账号访问,账号user 密码 123
在这里插入图片描述

项目源码地址

https://gitee.com/senhelpa-vivo/oauth2-sso.git

如果你觉得该博客对你有帮助,不放动动手指加一下交流群。

在这里插入图片描述

  • 7
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值