1.搭建SpringSecurityOauth2项目

  1. 使用idea就非常容易了 选择spring-security勾选oauth2依赖即可,需要注意的是版本兼容性是springboot2.3.10RELEASEidea在选择的时候会给出相应的提示,版本选对后我们来启动一下项目,确保项目能正常访问。项目启动时日志底下会输出密码,我们在浏览器上访问项目资源,我的是127.0.0.1:1213 会弹出一个login网页要求我们输入密码,默认用户是user,输入密码正确后我们的基础项目搭建就没有问题了。<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/2921283/1620115123119-dd6a4d9b-0c7c-417f-86c7-93bc916e6cea.png#clientId=u47d98f7b-7846-4&from=paste&height=267&id=u43650a94&margin=%5Bobject%20Object%5D&name=image.png&originHeight=534&originWidth=930&originalType=binary&size=27345&status=done&style=none&taskId=u0d9fc020-3d05-4818-a611-4581b3dc57b&width=465)<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/2921283/1620115142686-5e5866de-82f0-4498-808b-b37c97e8c302.png#clientId=u47d98f7b-7846-4&from=paste&height=319&id=ue4617d6b&margin=%5Bobject%20Object%5D&name=image.png&originHeight=637&originWidth=1619&originalType=binary&size=172629&status=done&style=none&taskId=u8eb9a532-24eb-4d25-93de-36ad511de63&width=809.5)

2. UserDetailsService

自定义登录逻辑后重启项目,输入密码时错误提示java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id “null”,在Spring Security中密码的存储格式是“{id}…………”。前面的id是加密方式,id可以是bcrypt、sha256等,后面跟着的是加密后的密码。也就是说,程序拿到传过来的密码的时候,会首先查找被“{”和“}”包括起来的id,来确定后面的密码是被怎么样加密的,如果找不到就认为id是null。这也就是为什么我们的程序会报错:There is no PasswordEncoder mapped for the id “null”。官方文档举的例子中是各种加密方式针对同一密码加密后的存储形式,原始密码都是“password”。

  1. {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
  2. {noop}password
  3. {pbkdf2}5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc
  4. {scrypt}$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc=
  5. {sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0
  1. /**
  2. * @author 杨胖胖
  3. */
  4. @Component
  5. @RequiredArgsConstructor(onConstructor = @__(@Autowired))
  6. public class UserDetailsServiceImpl implements UserDetailsService {
  7. @Override
  8. public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
  9. // todo 实际项目中这些内容需要从数据库表中查询到
  10. return new User(username, "{noop}123456", AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
  11. }
  12. }

如果不想用这种硬编码的方式我们也可以配置一个PasswordEncoder

  1. /**
  2. * @author 杨胖胖
  3. */
  4. @Configuration
  5. public class WebSecurityConfigurer{
  6. @Bean
  7. public PasswordEncoder passwordEncoder() {
  8. return new BCryptPasswordEncoder();
  9. }
  10. }

UserDetailsServiceImpl改成这样启动项目

  1. /**
  2. * @author 杨胖胖
  3. */
  4. @Component
  5. @RequiredArgsConstructor(onConstructor = @__(@Autowired))
  6. public class UserDetailsServiceImpl implements UserDetailsService {
  7. private final PasswordEncoder passwordEncoder;
  8. @Override
  9. public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
  10. // todo 实际项目中这些内容需要从数据库表中查询到
  11. return new User(username, passwordEncoder.encode("123456"), AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
  12. }
  13. }

3.AuthorizationServerConfigurerAdapter

1.配置密码模式与刷新令牌,我们将WebSecurityConfigurer继承WebSecurityConfigurerAdapter,密码模式需要authenticationManagerBean。

WebSecurityConfigurerAdapter

  1. @EnableWebSecurity
  2. public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
  3. @Bean
  4. public PasswordEncoder passwordEncoder() {
  5. return new BCryptPasswordEncoder();
  6. }
  7. @Bean
  8. @Override
  9. public AuthenticationManager authenticationManagerBean() throws Exception {
  10. return super.authenticationManagerBean();
  11. }
  12. }

2.继承AuthorizationServerConfigurerAdapter,以下是密码模式标配

  1. /**
  2. * @author 杨胖胖
  3. */
  4. @Configuration
  5. @EnableAuthorizationServer
  6. @RequiredArgsConstructor(onConstructor = @__(@Autowired))
  7. public class AuthorizationServerConfigurer extends AuthorizationServerConfigurerAdapter {
  8. private final PasswordEncoder passwordEncoder;
  9. private final AuthenticationManager authenticationManager;
  10. @Override
  11. public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
  12. clients
  13. // 使用内存存储
  14. .inMemory()
  15. //标记客户端id
  16. .withClient("cms")
  17. //客户端安全码
  18. .secret(passwordEncoder.encode("secret"))
  19. //允许授权范围
  20. .scopes("all")
  21. //token 时间秒
  22. .accessTokenValiditySeconds(7200)
  23. //刷新token 时间 秒
  24. .refreshTokenValiditySeconds(7200)
  25. //允许授权类型
  26. .authorizedGrantTypes("refresh_token", "password");
  27. }
  28. @Override
  29. public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
  30. endpoints
  31. .authenticationManager(authenticationManager)
  32. ;
  33. }
  34. }

我使用的是postman来测试
image.png
image.png


  • 新建资源来访问试试

    1. /**
    2. * @author 杨胖胖
    3. */
    4. @RestController
    5. public class CaptchaController {
    6. @GetMapping("captcha")
    7. public String captcha() {
    8. return "success";
    9. }
    10. }

  • 没有添加token直接访问

image.png


4.ResourceServerConfigurerAdapter

  • 配置资源服务器

    1. /**
    2. * @author 杨胖胖
    3. */
    4. @Configuration
    5. @EnableResourceServer
    6. public class ResourceServerConfigurer extends ResourceServerConfigurerAdapter {
    7. @Override
    8. public void configure(HttpSecurity http) throws Exception {
    9. http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
    10. .and()
    11. //请求权限配置
    12. .authorizeRequests()
    13. //下边的路径放行,不需要经过认证
    14. .antMatchers(PathsEnum.NOT_NEED_AUTHENTICATE.getPaths()).permitAll()
    15. //用户的增删改接口只允许管理员访问
    16. .antMatchers("/admin/*").hasAnyAuthority("admin")
    17. .anyRequest()
    18. .authenticated();
    19. }
    20. }
  • 加入token后访问资源

image.png


  • 关于WebSecurityConfigurer这段代码:
  • 解释一下优先级 ResourceServerConfigurerAdapter的HttpSecurity是高于WebSecurityConfigurerAdapterHttpSecurity的,所以理论上配置ResourceServerConfigurerAdapter的HttpSecurity就可以实现鉴权认证了

    1. @Override
    2. protected void configure(HttpSecurity http) throws Exception {
    3. http
    4. // 配置允许访问的链接
    5. .authorizeRequests().antMatchers("/oauth/**").permitAll()
    6. // 其余所有请求全部需要鉴权认证
    7. .anyRequest().authenticated()
    8. // 关闭跨域保护;
    9. .and().csrf().disable();
    10. }

    5.使用redis存储token信息

    ```java /**

    • @author 杨胖胖 */ @Configuration @RequiredArgsConstructor(onConstructor = @__(@Autowired)) public class TokenStoreConfig {

      private final RedisConnectionFactory redisConnectionFactory;

      @Bean public TokenStore redisTokenStore() {

      1. return new RedisTokenStore(redisConnectionFactory);

      }

}

  1. - AuthorizationServerConfigurer添加这行代码
  2. ```java
  3. @Override
  4. public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
  5. endpoints
  6. // 配置密码模式
  7. .authenticationManager(authenticationManager)
  8. // redis存储token
  9. .tokenStore(tokenStoreConfig.redisTokenStore())
  10. ;
  11. }

6.JWT替换默认令牌

  • 需要将redisTokenStore的Bean先注解掉否则会出现如下错误:

    1. Field tokenStore in org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfiguration required a single bean, but 2 were found:
    2. - redisTokenStore: defined by method 'redisTokenStore' in class path resource [com/example/security/config/TokenStoreConfig.class]
    3. - jwtTokenStore: defined by method 'jwtTokenStore' in class path resource [com/example/security/config/TokenStoreConfig.class]
  • TokenStoreConfig添加如下代码:

    1. @Bean
    2. public JwtTokenStore jwtTokenStore() {
    3. return new JwtTokenStore(accessTokenConverter());
    4. }
    5. @Bean
    6. public JwtAccessTokenConverter accessTokenConverter() {
    7. JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
    8. converter.setSigningKey("one eternity later");
    9. return converter;
    10. }
  • AuthorizationServerConfigurer替换成如下代码:

    1. public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
    2. endpoints
    3. // 配置密码模式
    4. .authenticationManager(authenticationManager)
    5. // 使用jwt签发token
    6. .tokenStore(tokenStoreConfig.jwtTokenStore())
    7. // token加签
    8. .accessTokenConverter(tokenStoreConfig.accessTokenConverter())
    9. ;
    10. }

  • 看一下效果:

image.png


7.JWT自定义内容

  1. /**
  2. * @author 杨胖胖
  3. */
  4. @Component
  5. public class TokenJwtEnhancer implements TokenEnhancer {
  6. @Override
  7. public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
  8. Map<String, Object> info = new HashMap<>();
  9. info.put("userId", "JayCou");
  10. ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(info);
  11. return accessToken;
  12. }
  13. }
  • AuthorizationServerConfigurer修改成如下代码:

    1. public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
    2. endpoints
    3. // 配置密码模式
    4. .authenticationManager(authenticationManager);
    5. // 配置自定义jwt
    6. TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
    7. List<TokenEnhancer> enhancers = new ArrayList<>();
    8. enhancers.add(tokenJwtEnhancer);
    9. enhancers.add(tokenStoreConfig.accessTokenConverter());
    10. enhancerChain.setTokenEnhancers(enhancers);
    11. endpoints.tokenEnhancer(enhancerChain).accessTokenConverter(tokenStoreConfig.accessTokenConverter());
    12. }
  • 效果如下:

image.png

  • 本来是想讲讲如何解析token的内容拿到自定义信息的,但是springSecurity2.3以上的版本默认加密非对称加密,翻阅了文献比较难解决这个所以就没整了,但是代码我贴出来了,以后有能力的话可以解决一下。

image.png


  • 解析自定义token所需要的依赖和代码
    1. <dependency>
    2. <groupId>org.apache.commons</groupId>
    3. <artifactId>commons-lang3</artifactId>
    4. <version>3.11</version>
    5. </dependency>
    6. <dependency>
    7. <groupId>io.jsonwebtoken</groupId>
    8. <artifactId>jjwt</artifactId>
    9. <version>0.7.0</version>
    10. </dependency>
    1. public Authentication get(Authentication authentication, HttpServletRequest request) {
    2. String header = request.getHeader("Authentication");
    3. String token = StringUtils.substringAfter(header, "bearer");
    4. Claims claims = Jwts.parser().setSigningKey("one eternity later".getBytes(StandardCharsets.UTF_8)).parseClaimsJws(token).getBody();
    5. Object userId = claims.get("userId");
    6. return authentication;
    7. }
    后续将更新:其它授权模式、拥有指定权限才能访问资源、接入数据库使用密码登录