Spring Boot整合 Spring Security、 OAuth2

Spring Security

1、Spring Security功能

身份认证,授权,防御常见攻击如csrf

2、RBAC 权限模型

RBAC模型(Role-Based Access Control:基于角色的访问控制)

在RBAC模型里面,有3个基础组成部分,分别是:用户、角色和权限,它们之间的关系如下图所示

SELECT * FROM sec_permission;
SELECT * FROM sec_role_permission ;
SELECT * FROM sec_role;
SELECT * FROM sec_user_role;
SELECT * FROM sec_user;

3、启动器依赖引入

啥配置也没做,啥类也没写。只是增加了一个启动器依赖

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

4、Hello接口

@RestController
public class HelloController {

    @RequestMapping("/hello")
    public String sayHello() {
        return "hello";
    }
}

在浏览器访问接口:​localhost:8080/hello​


5、实现原理

Spring Security对Web资源的保护是靠Servlet过滤器的

DefaultSecurityFilterChain

可以打断点看到默认加载的过滤器链:

可以在控制台启动日志里看到默认加载的过滤器:

Creating filter chain: 
org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@48c3a0ef,
org.springframework.security.web.context.SecurityContextPersistenceFilter@40cb95c1, 
org.springframework.security.web.header.HeaderWriterFilter@7744195, 
org.springframework.security.web.csrf.CsrfFilter@1df32c09, 
org.springframework.security.web.authentication.logout.LogoutFilter@2ff8d39b,
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@684a802a,
org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@64ef2719,
org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@3b4f1eb,
org.springframework.security.web.savedrequest.RequestCacheAwareFilter@5e2b512b,
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@4258106,
org.springframework.security.web.authentication.AnonymousAuthenticationFilter@6247c08c,
org.springframework.security.web.session.SessionManagementFilter@7d47b021, 
org.springframework.security.web.access.ExceptionTranslationFilter@1e429f56, 
org.springframework.security.web.access.intercept.FilterSecurityInterceptor@6fd97fd1

Spring Security用户名密码

1、默认用户名密码

登录页用户名默认:user

密码(随机生成)在服务启动时打印在了控制台

SecurityProperties


2、配置文件自定义

 当然我们也可以通过application.yml指定配置用户名密码

  • security.user.name 指定默认的用户名,默认为user.

  • security.user.password 默认的用户密码.

spring:
    security:
      user:
        name: admin
        password: admin

3、基于内存的登录认证

InMemoryUserDetailsManager 是 Spring Security 为我们提供的基于内存实现的 UserDetailsManager 。

方式一:基于UserDetailsService

@Configuration //标识配置类
@EnableWebSecurity //开启springsecurity的自定义配置(springboot项目中该注解可以省略,有预定义配置)
public class WebSecurityConfig {

    /**
     * 基于内存的认证方式
     *
     * @return
     */
    @Bean
    public UserDetailsService userDetailsService() {
        //基于内存的用户信息管理器
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        //使用manager管理UserDetails对象
        manager.createUser(
                User.withDefaultPasswordEncoder().username("xymao").password("111111").roles("admin").build()
        );
        return manager;
    }
}

此时在配置文件中的用户名和密码就不生效了

UsernamePasswordAuthenticationFilter在这个过滤器的attemptAuthentication()方法将输入的用户名密码和内存中的用户名密码做比较。


4、基于数据库的登录认证

管理员账号:admin/123456 

INSERT INTO `sec_user`
VALUES (1072806377661009920, 'admin', '$2a$10$64iuSLkKNhpTN19jGHs7xePvFsub7ZCcCmBqEYw8fbACGTE3XetYq', '管理员',
        '17300000000', 'admin@xkcoding.com', 785433600000, 1, 1, 1544611947032, 1544611947032);

JdbcUserDetailsManager

JdbcUserDetailsManager 是 Spring Security 为我们提供的基于数据库实现的 UserDetailsManager(持久层框架是jdbcTemplate)

方式一

Spring Security支持通过实现UserDetailsService接口的方式来提供用户认证授权信息。主要功能:根据用户名查询用户信息

@Service
public class CustomUserDetailsService implements UserDetailsService {
    @Autowired
    private UserDao userDao;

    @Autowired
    private RoleDao roleDao;

    @Autowired
    private PermissionDao permissionDao;

    @Resource
    private PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //通过用户名从数据库获取用户信息
        User user = userDao.findByUsername(username).orElseThrow(() -> new UsernameNotFoundException("未找到用户信息 : " + username));
        //定义权限列表
        List<GrantedAuthority> authorities = new ArrayList<>();
        authorities.add(new SimpleGrantedAuthority("a"));
        authorities.add(new SimpleGrantedAuthority("b"));
        authorities.add(new SimpleGrantedAuthority("c"));
        //返回spring security的User对象
        //user.getPassword() 数据库中的密码已经是密文存储
        return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), authorities);
    }
}

返回值类型是UserDetails,我们可以直接使用Spring Security提供的UserDetails接口实现类org.springframework.security.core.userdetails.User

方式二

参考基于内存校验:

  • 自定义类DBUserDetailsManager,实现UserDetailsManager, UserDetailsPasswordService接口。
  • 在校验用户时,springsecurity自动使用DBUserDetailsManager的loadUserByUsername方法从数据库中获取User对象
@Component
public class DBUserDetailsManager implements UserDetailsManager, UserDetailsPasswordService {

    @Autowired
    private UserDao userDao;


    @Override
    public UserDetails updatePassword(UserDetails userDetails, String s) {
        return null;
    }

    @Override
    public void createUser(UserDetails userDetails) {

    }

    @Override
    public void updateUser(UserDetails userDetails) {

    }

    @Override
    public void deleteUser(String s) {

    }

    @Override
    public void changePassword(String s, String s1) {

    }

    @Override
    public boolean userExists(String s) {
        return false;
    }


    /**
     * 从数据库中获取用户信息
     *
     * @param username
     * @return
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //通过用户名从数据库获取实体类User对象
        User user = userDao.findByUsername(username).orElseThrow(() -> new UsernameNotFoundException("未找到用户信息 : " + username));

        //定义权限列表
        List<GrantedAuthority> authorities = new ArrayList<>();
        //组装springSecurity中的User对象
        return new org.springframework.security.core.userdetails.User(
                user.getUsername(),
                user.getPassword(),
                true, //用户是否启用
                true, //用户账号是否未过期
                true, //用户凭证是否未过期
                true, //用户是否未被锁定
                authorities //权限列表
        );
    }
}

在springsecurity5.0之后引入了许多加密方式,因此在校验是需要指定一种加密方式.因此在WebSecurityConfig

@Configuration //标识配置类
@EnableWebSecurity //开启springsecurity的自定义配置(springboot项目中该注解可以省略,有预定义配置)
public class WebSecurityConfig {
    @Resource
    private DBUserDetailsManager dbUserDetailsManager;

    @Bean
    //密码加密,不然没法做密码加密对比
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 获取AuthenticationManager(认证管理器),登录时认证使用
     */
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }

    /**
     * AuthenticationProvider是认证逻辑的提供者
     *
     * @return
     */
    @Bean
    public AuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
        daoAuthenticationProvider.setUserDetailsService(dbUserDetailsManager);
        daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());
        return daoAuthenticationProvider;
    }
}

那一切真的这么顺利吗,这次是的。


5、默认配置

Java Configuration :: Spring Security

一下代码在WebSecurityConfig中加与不加都是一样的,是

@Configuration //标识配置类
@EnableWebSecurity //开启springsecurity的自定义配置(springboot项目中该注解可以省略,有预定义配置)
public class WebSecurityConfig {

    //默认配置
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        //组装过滤器链
        http.authorizeHttpRequests(
                authorize -> authorize
                        .anyRequest() //对所有请求开启授权保护
                        .authenticated() //已认证的请求会被自动授权
                 )
                .formLogin(withDefaults())//使用表单授权方式
                .httpBasic(withDefaults()); //使用基本授权方式
        return http.build();
    }
}

1


6、关闭security验证

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().permitAll().and().logout().permitAll();//配置不需要登录验证
    }
}

WebSecurityConfigurerAdapter 是由Spring Security提供的Web应用安全配置的适配器

WebSecurityConfigurerAdapter 是一个适配器类,允许开发者通过重写特定的方法来自定义其 Web 安全配置

创建一个配置类WebSecurityConfig继承WebSecurityConfigurerAdapter这个抽象类并重写configure(HttpSecurity http)方法,可以精确地定义哪些URL可以由哪些角色访问。

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin() // 表单方式
                .and()
                .authorizeRequests() // 授权配置
                .anyRequest().authenticated(); //所有未匹配的请求都需要用户进行身份验证
    }
}

Spring Security提供了这种链式的方法调用。上面配置指定了认证方式为表单登录,并且所有请求都需要进行认证。

HttpSecurity 是 Spring Security 中用于构建安全配置的一个类。通过该类,开发者可以配置许多与 HTTP 安全相关的选项,如认证、授权、CORS、CSRF 保护等

.formLogin() 是 HttpSecurity 类的一个方法,用于启用基于表单的身份验证。当你调用这个方法时,Spring Security 会自动配置登录表单的相关设置,如登录页面的 URL、登录成功和失败的处理等。你可以进一步定制这些设置,以适应你的应用程序需求。

-------------------------------

http.authorizeRequests() 是 HttpSecurity 类的一个方法,用于定义 URL 的访问权限。通过该方法,你可以指定哪些 URL 需要特定的角色或权限才能访问,哪些 URL 可以公开访问等。

--------------

.anyRequest().authenticated() 表示所有未匹配的请求都需要用户进行身份验证。


用户操作

1、新增用户

在HelloController中加入方法

    @Resource
    private DBUserDetailsManager dbUserDetailsManager;  

    @PostMapping("/adduser")
    public void addUser(@RequestBody User user) {
        UserDetails userDetails = org.springframework.security.core.userdetails.User
                .withDefaultPasswordEncoder()
                .username(user.getUsername())
                .password(user.getPassword())
                .build();
        dbUserDetailsManager.createUser(userDetails);
    }

重写DBUserDetailsManager中的createUser方法

    //向数据库中插入用户信息
    @Override
    public void createUser(UserDetails userDetails) {
        User user = new User();
        user.setUsername(userDetails.getUsername());
        user.setPassword(userDetails.getPassword());
        userDao.save(user);
    }

引入swagger的api,进行用户新增测试

<!-- swagger-->
<dependency>
    <groupId>com.github.xiaoymin</groupId>
    <artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
    <version>4.1.0</version>
</dependency>

localhost:8080/doc.html


Spring Security访问授权

1、授权GrantedAuthority

GrantedAuthority则表示用户验证通过后被授予的权限。

SimpleGrantedAuthority

SimpleGrantedAuthority是默认的授权实现,它只存储权限(存储授予Authentication对象的权限的String表示形式),是一种简易的授权实现。

  • GrantedAuthority:直译"授予权限"
  • Authentication:直译"验证"

给我的感觉就是权限就是一个字符串,难道什么样的字符串都行吗?为啥定义的这么模糊

那我们就姑且给他"a","b","c"。。看看它怎么说

AuthorityUtils:此类一般用于UserDetailsService的实现类中的loadUserByUsername方法

作用为给user账户添加一个或多个权限,用逗号分隔,底层调用的是createAuthorityList方法,唯一区别在于此方法把所有的权限包含进一个字符串参数中,只不过用逗号分隔。

@Service
public class UserDetailsServiceImpl implements UserDetailsService{
	
	@Autowired
	PasswordEncoder passwordEncoder;
	
	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		//比较密码
		String pass=passwordEncoder.encode("123");//加密
		return new User(username,pass,AuthorityUtils.commaSeparatedStringToAuthorityList("admin,normal"));
	}
	
}

createAuthorityList

将权限转换为List,如

@Service
public class UserDetailsServiceImpl implements UserDetailsService{
		
	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

		List<GrantedAuthority> list=AuthorityUtils.createAuthorityList("admin","normal");//一个权限一个参数
		return new User(username,pass,list);
	}
}

1


2、配置类中配置

实际项目中我们不会把密码明文存储在数据库中。只需要使用把BCryptPasswordEncoder对象注入Spring容器中,SpringSecurity就会使用该PasswordEncoder来进行密码校验

Spring Security实现的BCryptPasswordEncoder已经足够强大,它对相同的密码进行加密后可以生成不同的结果

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Resource
    private UserDetailsService userDetailsService;

    @Bean
    public PasswordEncoder passwordEncoder() {
        //使用默认的BCryptPasswordEncoder加密方案
        return new BCryptPasswordEncoder();
    }

    /**
     * 配置用户详细信息的服务和密码编码器
     *
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //数据库读取的用户进行身份认证
        auth.userDetailsService(userDetailsService)
                .passwordEncoder(passwordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin() // 表单方式
                .and()
                .authorizeRequests() // 授权配置
                .anyRequest().authenticated(); //所有未匹配的请求都需要用户进行身份验证
    }
}

Spring Security中的BCryptPasswordEncoder方法采用SHA-256 +随机盐+密钥对密码进行加密。SHA系列是Hash算法,不是加密算法,使用加密算法意味着可以解密(这个与编码/解码一样),但是采用Hash处理,其过程是不可逆的。

1)加密(encode):注册用户时,使用SHA-256+随机盐+密钥把用户输入的密码进行hash处理,得到密码的hash值,然后将其存入数据库中。

2)密码匹配(matches):用户登录时,密码匹配阶段并没有进行密码解密(因为密码经过Hash处理,是不可逆的),而是使用相同的算法把用户输入的密码进行hash处理,得到密码的hash值,然后将其与从数据库中查询到的密码hash值进行比较。如果两者相同,说明用户输入的密码正确。

再次访问接口:http://127.0.0.1:8089/hello

使用账号密码登录 admin/123456


3、权限控制

Spring Security支持方法级别的权限控制。在此机制上,我们可以在任意层的任意方法上加入权限注解,加入注解的方法将自动被Spring Security保护起来,仅仅允许特定的用户访问,从而还到权限控制的目的

@PreAuthorize() 该注解用于方法前验证权限

//使用权限注解标明只有拥有“admin”权限的人才能访问:       

@PreAuthorize("hasAuthority('admin')")
@RestController
public class HelloController {

    @RequestMapping("/hello")
    public String sayHello() {
        return "hello";
    }

    @RequestMapping("/a")
    @PreAuthorize("hasAuthority('a')")
    public String sayA() {
        return "aaaaa";
    }

    @RequestMapping("/d")
    @PreAuthorize("hasAuthority('d')")
    public String sayB() {
        return "ddddd";
    }
}

Spring Security默认是禁用注解的,要想开启注解,要在继承WebSecurityConfigurerAdapter的类加@EnableGlobalMethodSecurity()注解,并在该类中将AuthenticationManager定义为Bean。说实话我没有注入AuthenticationManager这个bean的时候,也做到了权限校验。。这到底有啥用?

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Resource
    private UserDetailsService userDetailsService;

    @Bean
    public PasswordEncoder passwordEncoder() {
        //使用默认的BCryptPasswordEncoder加密方案
        return new BCryptPasswordEncoder();
    }

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

    /**
     * 配置用户详细信息的服务和密码编码器
     *
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //数据库读取的用户进行身份认证
        auth.userDetailsService(userDetailsService)
                .passwordEncoder(passwordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin() // 表单方式
                .and()
                .authorizeRequests() // 授权配置
                .anyRequest().authenticated(); //所有未匹配的请求都需要用户进行身份验证
    }
}

我们看到@EnableGlobalMethodSecurity 分别有prePostEnabled 、securedEnabled、jsr250Enabled三个字段,其中每个字段代码一种注解支持,默认为false,true为开启

重新登录访问。记得之前我随便给的权限字符串a,b,c。。。访问a是没问题的

访问d会返回403错误码。

自定义权限不足处理器来处理权限不足时候的操作


4、Session管理

用户登录成功后,信息保存在服务器Session中。如Tomcat

登录后,可以看到cookie中存储了JSESSIONID的cookie。

Session超时设置

如可以设置session有效期为1小时

server:
  session:
    timeout: 3600

这时候,就涉及到一个session共享

当应用集群部署的时候,用户在A应用上登录认证了,后续通过负载均衡可能会把请求发送到B应用,而B应用服务器上并没有与该请求匹配的认证Session信息,所以用户就需要重新进行认证

Spring Security默认的退出登录URL为/logout

自定义配置

1、请求授权

http.authorizeRequests()主要是对url进行访问权限控制,通过这个方法来实现url授权操作。支持链式写法,举例:
 http.authorizeRequests() .url匹配规则1.权限控制方法1 .url匹配规则2.权限控制方法2...

​在所有匹配规则中取所有规则的交集。配置顺序影响了之后授权效果。

越是具体的应该放在前面,越是笼统的应该放到后面。

anyRequest(),表示匹配所有的url请求

如下图表示:所有请求都不需要验证

与之相反的如下,表示所有请求都需要登录验证

antMatcher(String regx)

antMatcher(String regx),传递一个ant表达式参数,表示匹配所有满足ant表达式的请求

ant表达式中特殊字符解释

举例:

/a/* :匹配 /a/ 下的所有路径,不递归,如: /a/b,/a/c,但不包括/a/b/c

/a/**:匹配 /a/ 下所有路径,递归,如 :/a/b,/a/b/c,/a/b/c/d

/a/a?c:匹配/a/ 下路径中a开头,c结尾,中间按含任意字符的路径,如:/a/adc


2、访问控制方法

permitAll()    表示所匹配的URL任何人都允许访问
anonymous()    表示可以匿名访问匹配的URL。和permitAll()效果类似,只是设置为anonymous()的url会执行filterChain中的filter
denyAll()    表示所匹配的URL都不允许被访问。
authenticated()    表示所匹配的URL都需要被认证才能访问
rememberMe()    允许通过remember-me登录的用户访问
access()    SpringEl表达式结果为true时可以访问
fullyAuthenticated()    用户完全认证可以访问(非remember-me下自动登录)
hasRole()    如果有参数,参数表示角色,则其角色可以访问
hasAnyRole()    如果有参数,参数表示角色,则其中任何一个角色可以访问
hasAuthority()    如果有参数,参数表示权限,则其权限可以访问
hasAnyAuthority()    如果有参数,参数表示权限,则其中任何一个权限可以访问
hasIpAddress()    如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问


Spring Security OAuth2

1、什么是OAuth

OAuth是一种用来规范令牌(Token)发放的授权机制,主要包含了四种授权模式:授权码模式、简化模式、密码模式和客户端模式

OAuth相关的名词

  1. Third-party application 第三方应用程序,比如这里的虎牙直播;

  2. HTTP service HTTP服务提供商,比如这里的QQ(腾讯);

  3. Resource Owner 资源所有者,就是QQ的所有人,你;

  4. User Agent 用户代理,这里指浏览器;

  5. Authorization server 认证服务器,这里指QQ提供的第三方登录服务;

  6. Resource server 资源服务器,这里指虎牙直播提供的服务,比如高清直播,弹幕发送等(需要认证后才能使用)。

Spring Security OAuth2主要包含认证服务器和资源服务器这两大块的实现:

认证服务器主要包含了四种授权模式的实现和Token的生成与存储

资源服务器主要是在Spring Security的过滤器链上加了OAuth2AuthenticationProcessingFilter过滤器,即使用OAuth2协议发放令牌认证的方式来保护我们的资源


2、认证授权服务器

创建认证服务器很简单,只需要在Spring Security的配置类上使用@EnableAuthorizationServer注解标注即可

使用 @EnableAuthorizationServer 注解,在应用中自动开启和配置 Spring Security OAuth 的授权服务组件。

 @EnableAuthorizationServer 注解主要是导入两个配置类,分别是:

  • AuthorizationServerEndpointsConfiguration,这个配置类主要配置授权端点,获取token的端点。大家就把对应的端点想象成controller即可,在这个controller下开放了若干个@RequestMapping,比如常见的有:/oauth/authorize(授权路径)/oauth/token(获取token)
  • AuthorizationServerSecurityConfiguration,主要是做spring-security的安全配置

3、资源服务器

资源服务器的配置也很简单,只需要在配置类上使用@EnableResourceServer注解标注即可:

通过资源服务器来保护我们指定的资源,必须在获取授权认证的时候才能访问。在SpringBoot当中,我们可以通过@EnableResourceServer注解来开启此功能。

    @Configuration
    @EnableResourceServer
    public class ResourceConfigure extends ResourceServerConfigurerAdapter {
    
        @Override
        public void configure(HttpSecurity http) throws Exception {
            http.csrf().disable().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
                    .and().authorizeRequests().antMatchers("/free/**").permitAll().and()
                    .authorizeRequests().anyRequest().authenticated()
                    .and().formLogin().permitAll();//必须认证过后才可以访问
        }
    }
    

JsonWebToken

JWT,全称是Json Web Token, 是JSON风格轻量级的授权和身份认证规范,可实现无状态、分布式的Web应用授权;

1、JWT介绍

JWT.io 是一个方便、快速、实用的 JWT 在线解析工具,在进行开发和调试时可以帮助我们解析和验证 JWT。JSON Web Tokens - jwt.io

JWT(Json Web Token)就是一个字符串,由三部分构成:

  • Header(头部)

  • Payload(载荷)

  • Signature(签名)

头 Header:  加密算法(arg)+类型(typ)

然后将Header进行Base64编码(该编码是可以对称解码的),构成了第一部分。

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

载荷就是存放有效信息的地方。定义细节如下:

有效载荷 Payload

JWT指定七个默认载荷字段供选择。

iss:发行人

exp:过期时间

sub:主题

aud:用户

nbf:在此之前不可用

iat:发布时间

jti:JWT ID用于标识该JWT

除以上默认字段外,我们还可以自定义载荷私有字段,如(用户id、头像地址、昵称)

{
"nickname": "xiaoyumao",
"id": "1001",
"avatar": "yy.jpg"
}

然后将其进行Base64编码,得到Jwt的第二部分:

eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ

签名哈希部分是对上面两部分数据签名,通过指定的算法生成哈希,以确保数据不会被篡改。

签名 Signature

这个部分需要Base64编码后的Header和Base64编码后的Payload使用 . 连接组成的字符串,然后通过Header中声明的加密方式进行加密

然后就构成了jwt的第三部分。密码$secret 仅仅为保存在服务器中,并且不能向用户公开。

var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);
var signature = HMACSHA256(encodedString, '$secret');

2、生成jwt令牌

JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.7.0</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <version>2.0.6.RELEASE</version>
</dependency>

生成jwt令牌,即base64编码后的字符串

  • JwtBuilder setClaims(Claims claims);
  • JwtBuilder setClaims(Map<String, Object> claims);
    不能就是说,我们也可以直接传入map值对象。
@SpringBootTest
public class JWTtest {
    @Test
    public void contextLoads() {
        //构建jwt 字符串
        String jwt = Jwts.builder()
                //请求头   加密算法和令牌类型
                .setHeaderParam("alg", "HS256")
                .setHeaderParam("typ", "jwt")
                //载荷
                .setSubject("GMALL-TOKEN") //主题
                .setExpiration(new Date(System.currentTimeMillis() + 60 * 1000 * 60)) //什么时候过期
                .claim("nickname", "xiaoyumao")
                .claim("id", "1001")
                .claim("avatar", "yy.jpg")
                //签名
                .signWith(SignatureAlgorithm.HS256, "123456")
                .compact();
        System.out.println(jwt);
    }
}

eyJhbGciOiJIUzI1NiIsInR5cCI6Imp3dCJ9.eyJzdWIiOiJHTUFMTC1UT0tFTiIsImV4cCI6MTY3NDIwODcyMiwibmlja25hbWUiOiJ4aWFveXVtYW8iLCJpZCI6IjEwMDEiLCJhdmF0YXIiOiJ5eS5qcGcifQ.5WFPxldISkQ8RxGBGFGTtrdjpzXLe6TdHKkc6h3aROo


3、解析jwt字符串

@Test
public void test(){
    String s = "";//s是刚刚生成的jwt字符串
    Jwt jwt = Jwts.parser().setSigningKey("123456")
            .parse(s);
    DefaultClaims body = (DefaultClaims) jwt.getBody();
    System.out.println(body.get("id"));
    System.out.println(body.get("nickname"));
    System.out.println(body.get("avatar"));
}

后端将来只要验证jwt有效载荷数据使用秘钥再次加密生成的签名和jwt的签名 就能判断数据是否被篡改;就算签名验证成功了,再验证过期时间 如果过期了也报错


4、JWT工具类生成token

JWT的本质就是一个字符串,它是将用户信息保存到一个Json字符串中,然后进行编码后得到一个JWT token并且这个JWT token带有签名信息,接收后可以校验是否被篡改。

一般是将它放入HTTP请求的Header Authorization字段中。

1.构建公钥、私钥文件,这个secret你跟别人越不一样,就越安全

    String publicKeyFile = "E:\\rsa.pub";
    String privateKeyFile = "E:\\rsa.pri";
    String secret = "`qa*97()'!dasfa213";

    @Test
    void contextLoads() throws Exception {
        //构建公钥私钥文件
        RsaUtils.generateKey(publicKeyFile,privateKeyFile , secret);
    }

2.加载公钥私钥文件生成对象

PublicKey publicKey;
PrivateKey privateKey;
int expire = 60;
//加载公钥私钥文件生成对象
@Test
void test1() throws Exception {
    publicKey = RsaUtils.getPublicKey(publicKeyFile);
    privateKey = RsaUtils.getPrivateKey(privateKeyFile);
}

3.使用私钥对象生成jwt字符串,map是载荷

//生成jwt
@Test
void generateJwt() throws Exception {
    Map<String, Object> map = new HashMap<>();
    map.put("userId","1001");
    map.put("username","xiaoyumao");
    map.put("avatar","mengmeng.jpg");
    //使用私钥对象生成jw  map是载荷
    String token = JwtUtils.generateToken(map, privateKey, expire);
    System.out.println(token);
}

4.使用公钥对象解析jwt字符串

 @Test
 void parseJwt() throws Exception {
     String jwt = "eyJhbGciOiJ...";//上一步生成的jwt字符串,很长很长,这里简写
     Map<String, Object> map = JwtUtils.getInfoFromToken(jwt, publicKey);
     System.out.println(map);
 }

公钥或者私钥任何一个不存在,都要重新构建密钥对


5、JwtConfig

application.yml

jwt:
  config:
    key: xkcoding
    ttl: 600000
    remember: 604800000

创建配置绑定类

@ConfigurationProperties(prefix = "jwt.config")
@Data
public class JwtConfig {
    /**
     * jwt 加密 key,默认值:xkcoding.
     */
    private String key = "xkcoding";

    /**
     * jwt 过期时间,默认值:600000 {@code 10 分钟}.
     */
    private Long ttl = 600000L;

    /**
     * 开启 记住我 之后 jwt 过期时间,默认值 604800000 {@code 7 天}
     */
    private Long remember = 604800000L;
}

加密key,在生产签名时使用

@EnableConfigurationProperties(JwtConfig.class)
@Configuration
@Slf4j
public class JwtUtil {
    @Autowired
    private JwtConfig jwtConfig;

//.signWith(SignatureAlgorithm.HS256, jwtConfig.getKey())

}

oAuth认证

1、依赖和配置

        <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>

1


2、密码模式

认证服务器和资源服务器可以在同一台服务器上,比如前后端分离的服务后台,它即供认证服务(认证服务器,提供令牌),客户端通过令牌来从后台获取服务(资源服务器);它们也可以不在同一台服务器上

客户端向认证服务器申请令牌的HTTP请求,包含以下参数:(密码模式)

在密码模式中,客户端通过用户名和密码到认证服务器获取令牌。客户端发出的HTTP请求,包含以下参数:

  1. grant_type:表示授权类型,此处的值固定为”password”,必选项。

  2. username:表示用户名,必选项。

  3. password:表示用户的密码,必选项。

资源服务器主要是在Spring Security的过滤器链上加了OAuth2AuthenticationProcessingFilter过滤器,即使用OAuth2协议发放令牌认证的方式来保护我们的资源

建认证服务器很简单,只需要在Spring Security的配置类上使用@EnableAuthorizationServer注解标注即可。创建

同样使用postman发送POST请求localhost:8080/oauth/token:

grant_type填password,表示密码模式


3、BCryptPasswordEncoder

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends WebSecurityConfigurerAdapter {

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

4、refresh_token

client_id为test1的refresh_token(下面会介绍到)有效时间为864000秒,即10天,也就是说在这10天内都可以通过refresh_token来换取新的令牌;

1

我们获取到的令牌都是基于Spring Security OAuth2默认配置生成的,Spring Security允许我们自定义令牌配置

令牌的有效时间,令牌的存储策略等

认证服务器AuthorizationServerConfig继承AuthorizationServerConfigurerAdapter,并重写它的configure(ClientDetailsServiceConfigurer clients)方法:

 @Autowired
    private TokenStore redisTokenStore;

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        endpoints.authenticationManager(authenticationManager)
            .tokenStore(redisTokenStore);
    }

默认令牌是存储在内存中的,我们可以将它保存到第三方存储中,比如Redis。


5、Token增强器

如果想在JWT中添加一些额外的信息,我们需要实现TokenEnhancer(Token增强器)

public class JWTokenEnhancer implements TokenEnhancer {
    @Override
    public OAuth2AccessToken enhance(OAuth2AccessToken oAuth2AccessToken, OAuth2Authentication oAuth2Authentication) {
        Map<String, Object> info = new HashMap<>();
        info.put("message", "hello world");
        ((DefaultOAuth2AccessToken) oAuth2AccessToken).setAdditionalInformation(info);
        return oAuth2AccessToken;
    }
}

6、使用JWT替换默认令牌

使用JWT替换默认的令牌(默认令牌使用UUID生成)只需要指定TokenStore为JwtTokenStore即可。

要在Java代码中解析JWT,需要添加如下依赖:

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>
Jwts.parser().setSigningKey("test_key".getBytes(StandardCharsets.UTF_8)).parseClaimsJws(token).getBody();

signkey需要和JwtAccessTokenConverter中指定的签名密钥一致。重启项目,获取令牌后访问/index。

  • 18
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是一个简单的 Spring Boot 整合 Spring Security OAuth2 的代码示例: 1. 添加依赖 在 pom.xml 文件中添加以下依赖: ``` <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.security.oauth</groupId> <artifactId>spring-security-oauth2</artifactId> <version>2.3.3.RELEASE</version> </dependency> ``` 2. 配置 OAuth2 客户端 在 application.yml 文件中添加以下配置: ``` security: oauth2: client: clientId: your-client-id clientSecret: your-client-secret accessTokenUri: https://your-auth-server.com/oauth/token userAuthorizationUri: https://your-auth-server.com/oauth/authorize clientAuthenticationScheme: form scope: read write ``` 3. 配置 Spring Security 创建一个继承自 WebSecurityConfigurerAdapter 的类,并添加以下配置: ``` @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/oauth/**").permitAll() .anyRequest().authenticated() .and() .formLogin().permitAll() .and() .csrf().disable(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() .withUser("user").password("{noop}password").roles("USER"); } } ``` 4. 创建资源服务器 创建一个继承自 ResourceServerConfigurerAdapter 的类,并添加以下配置: ``` @Configuration @EnableResourceServer public class ResourceServerConfig extends ResourceServerConfigurerAdapter { @Override public void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/api/**").authenticated() .anyRequest().permitAll(); } @Override public void configure(ResourceServerSecurityConfigurer resources) throws Exception { resources.resourceId("resource-id"); } } ``` 5. 创建控制器 创建一个简单的控制器,用于测试 OAuth2 认证: ``` @RestController public class TestController { @GetMapping("/api/test") public String test() { return "Hello, World!"; } } ``` 6. 运行应用程序 现在你可以运行应用程序,并访问 http://localhost:8080/api/test 进行测试。如果你没有提供正确的 OAuth2 认证信息,你将会收到一个 401 Unauthorized 错误。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值