Bootstrap

【安全框架】Spring Security、Oauth2、JWT 这一篇就够了

文章目录

Spring Security

Spring家族一员。是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反转Inversion of Contro1,DI: Dependency Injection依赖注入)和AOP(面向切面编程〉功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。
image-20220114152035893

1. 安全框架概述

什么是安全框架?解决系统安全问题的框架。如果没有安全框架,我们需要手动处理每个资源的访问控制,非常麻烦。使用安全框架,我们可以通过配置的方式实现对资源的访问限制。

2. Spring Security 简述

Spring Security是一个高度自定义的安全框架。利用Spring loC/DI和AOP功能,为系统提供了声明式安全访问控制功能,减少了为系统安全而编写大量重复代码的工作。使用Spring Secruity的原因有很多,但大部分都是发现了javaEE的Servlet规范或EJB规范中的安全功能缺乏典型企业应用场景。同时认识到他们在WAR 或EAR 级别无法移植。因此如果你更换服务器环境,还有大量工作去重新配置你的应用程序。使用Spring Security解决了这些问题,也为你提供许多其他有用的、可定制的安全功能。正如你可能知道的两个应用程序的两个主要区域是“认证"和"授权”(或者访问控制)。这两点也是Spring Security重要核心功能。

认证是建立一个他声明的主体的过程(一个"主体"一般是指用户,设备或一些可以在你的应用程序中执行动作的其他系统),通俗点说就是系统认为用户是否能登录。

授权指确定一个主体是否允许在你的应用程序执行一个动作的过程。通俗点讲就是系统判断用户是否有权限去做某些事情。

3.Spring Security

3.1 创建项目

image-20220114154457373

3.2 项目依赖

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

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

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

        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>

	</dependencies> 

3.3 页面

3.3.1 login.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登录</title>
</head>
<body>
<form action="" method="post" style="width: 25%;margin: 60px auto;">
    <label> 账号:
        <input type="text" name="username">
    </label><br><br>
    <label> 密码:
    <input type="password" name="password">
    </label><br><br>
    <input type="submit" value="登录" style="width: 100px;">
</form>
</body>
</html>
3.3.2 main.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Home</title>
</head>
<body>
<h1>登录成功</h1>
</body>
</html>

3.4 测试

3.4.1 启动项目

image-20220114155729200

3.4.2 打开浏览器
http://localhost:8080/login.html

初始账号 user

初始密码每次启动都会改变
image-20220117230622137

会跳转到Spring Security 提供的登陆页面

image-20220114155958698

注:没有做任何配置

3.5 自定义登录逻辑

3.5.1 Security 的配置类
package com.yuan.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;

/**
 * @Author 小源同学
 * @Date 2022 01 14 16 17
 * @Describe  Security 的配置类
 **/
@Configuration
public class SecurityConfig {

    @Bean
    public PasswordEncoder getPassWord() {
        return new BCryptPasswordEncoder();
    }
}
3.5.2 UserDetailsService 的实现类
package com.yuan.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

/**
 * @Author 小源同学
 * @Date 2022 01 14 16 24
 * @Describe UserDetailsService 的实现类
 **/
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        System.out.println("执行了loadUserByUsername方法");
        //1.查询数据库,用户名称/账号是否存在,如果不存在就抛出UsernameNotFoundException异常
        if (!"admin".equals(username)) throw new UsernameNotFoundException("用户名不存在");
        //2.把查询的密码(注册时加密过)进行解析,或者直接把密码放入构造方法
        String password = passwordEncoder.encode("123");
        return new User(username, password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin,normal,ROLE_abc"));
    }
}
3.5.3 重启测试
http://localhost:8080/login.html

输入一个错的

image-20220114163130128

输入正确的之后跳到了我们自己写的登陆页面

image-20220114163353331

3.6 自定义登陆页面

3.6.1 login.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<form action="/login" method="post" style="width: 25%;margin: 60px auto;">
    <label>
        用户名:
        <input type="text" name="username">
    </label>
    <br><br>
    <label>&nbsp;&nbsp;&nbsp;码:
        <input type="password" name="password">
    </label> <br><br>
    <label>
        记住我:
        <input type="checkbox" name="remember-me" value="true">
    </label>
    <br><br>
    <br><br>
    <input type="submit" value="登录">
</form>
</body>
</html>
3.6.2 Security 的配置类
package com.yuan.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;

/**
 * @Author 小源同学
 * @Date 2022 01 14 16 17
 * @Describe Security 的配置类
 **/
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //自定义登录页面
        http.formLogin()
                //当发现、login的时候认为是登录,去执行 UserDetailsServiceImpl->loadUserByUsername
                .loginProcessingUrl("/login")
                //自定义登录页面
                .loginPage("/login.html")
                //登陆成功要跳转的页面,必须是post请求
                .successForwardUrl("/toMain")
        ;

        //设置了自定义登陆页面之后,security提供的原始的认证将全部失效
        //授权认证
        http.authorizeRequests()
                //登录页面,登陆失败页面都不需要认证
                .antMatchers("/login.html").permitAll()
                //所以要对所有的请求做拦截做认证【必须是登录之后才能被访问】
                .anyRequest().authenticated()
        ;

        //暂时理解为防火墙
        //关闭csrf防护
        http.csrf().disable();
    }

    @Bean
    public PasswordEncoder getPassWord() {
        return new BCryptPasswordEncoder();
    }
}
3.6.3 Controller
@RequestMapping("/toMain")
public String toMain() {
    return "redirect:main.html";
}
3.6.4 重启测试
http://localhost:8080/login.html

image-20220114170609743

登陆后自动跳转

image-20220114170727493

3.7 自定义失败页面

3.7.1 error.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>错误</title>
</head>
<body>
<div style="text-align: center;">
    <h1>登录失败</h1><a href="/login.html">重新登录</a>
</div>
</body>
</html>
3.7.2 Security 的配置类
package com.yuan.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;

/**
 * @Author 小源同学
 * @Date 2022 01 14 16 17
 * @Describe Security 的配置类
 **/
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //自定义登录页面
        http.formLogin()
                //当发现、login的时候认为是登录,去执行 UserDetailsServiceImpl->loadUserByUsername
                .loginProcessingUrl("/login")
                //自定义登录页面
                .loginPage("/login.html")
                //登陆成功要跳转的页面,必须是post请求
                .successForwardUrl("/toMain")
                //登陆失败要跳转的页面,必须是post请求
                .failureForwardUrl("/toError")
        ;

        //设置了自定义登陆页面之后,security提供的原始的认证将全部失效
        //授权认证
        http.authorizeRequests()
                //登录页面,登陆失败页面都不需要认证
                .antMatchers("/login.html","/error.html").permitAll()
                //所以要对所有的请求做拦截做认证【必须是登录之后才能被访问】
                .anyRequest().authenticated()
        ;

        //暂时理解为防火墙
        //关闭csrf防护
        http.csrf().disable();
    }

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

3.7.3 Controller
    @RequestMapping("/toError")
    public String toError() {
        return "redirect:error.html";
    }

3.7.4 重启测试

http://localhost:8080/login.html

image-20220114172121685

登陆失败后跳转

image-20220114172150611

3.8 设置请求账户和密码的参数名

3.8.1 login.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<form action="/login" method="post" style="width: 25%;margin: 60px auto;">
    <label>
        用户名:
        <input type="text" name="usernamercy">
    </label>
    <br><br>
    <label>&nbsp;&nbsp;&nbsp;码:
        <input type="password" name="passwordrcy">
    </label> <br><br>
    <label>
        记住我:
        <input type="checkbox" name="remember-me" value="true">
    </label>
    <br><br>
    <br><br>
    <input type="submit" value="登录">
</form>
</body>
</html>
3.8.2 Security 的配置类
package com.yuan.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;

/**
 * @Author 小源同学
 * @Date 2022 01 14 16 17
 * @Describe Security 的配置类
 **/
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //自定义登录页面
        http.formLogin()
                /*
                  <input type="text" name="usernamercy">
                  <input type="password" name="passwordrcy">
                  与name相对应
                */
                .usernameParameter("usernamercy")
                .passwordParameter("passwordrcy")
                //当发现、login的时候认为是登录,去执行 UserDetailsServiceImpl->loadUserByUsername
                .loginProcessingUrl("/login")
                //自定义登录页面
                .loginPage("/login.html")
                //登陆成功要跳转的页面,必须是post请求
                .successForwardUrl("/toMain")
                //登陆失败要跳转的页面,必须是post请求
                .failureForwardUrl("/toError")
        ;

        //设置了自定义登陆页面之后,security提供的原始的认证将全部失效
        //授权认证
        http.authorizeRequests()
                //登录页面,登陆失败页面都不需要认证
                .antMatchers("/login.html", "/error.html").permitAll()
                //所以要对所有的请求做拦截做认证【必须是登录之后才能被访问】
                .anyRequest().authenticated()
        ;

        //暂时理解为防火墙
        //关闭csrf防护
        http.csrf().disable();
    }

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

3.9 自定义登陆成功处理器

3.9.1 MyAuthenticationSuccessHandler
package com.yuan.handle;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @Author 小源同学
 * @Date 2022 01 14 18 34
 * @Describe 自定义登陆成功处理器
 **/
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    /*重定向的连接*/
    private String success_url;

    public MyAuthenticationSuccessHandler(String success_url) {
        this.success_url = success_url;
    }

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        System.out.println(request.getRemoteAddr());
        System.out.println(request.getRemoteAddr());
        User principal = (User) authentication.getPrincipal();
        System.out.println(principal.getUsername());
        System.out.println(principal.getPassword());//因为安全的原因输出null
        System.out.println(principal.getAuthorities());//权限
        response.sendRedirect(success_url);
    }
}

3.9.2 Security 的配置类
//                .successForwardUrl("/toMain")
                .successHandler(new MyAuthenticationSuccessHandler("https://www.baidu.com/"))

3.10 自定义登陆失败处理器

3.10.1 MyAuthenticationFailureHandler
package com.yuan.handle;

import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @Author 小源同学
 * @Date 2022 01 14 18 42
 * @Describe 自定义登陆失败处理器
 **/
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
    private String failure_url;

    public MyAuthenticationFailureHandler(String failure_url) {
        this.failure_url = failure_url;
    }

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        response.sendRedirect(failure_url); 
    }
}

3.10.2 Security 的配置类
//                .failureForwardUrl("/toError")
                .failureHandler(new MyAuthenticationFailureHandler("/error.html"))

3.11 权限判断

3.11.1 main1.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>main1</title>
</head>
<body>
main1.html
</body>
</html>
3.11.2 Security 的配置类
     .antMatchers("/main1.html").hasAuthority("admin")

3.12 角色判断

3.12.1 Security 的配置类
//                .antMatchers("/main1.html").hasRole("abc")
                .antMatchers("/main1.html").hasAnyRole("abc","123","chat")

3.13 IP地址判断

3.13.1 Security 的配置类
 .antMatchers("/main1.html").hasIpAddress("127.0.0.1")

3.14 自定义403处理方案

3.14.1 MyAccessDeniedHandler
package com.yuan.handle;

import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

/**
 * @Author 小源同学
 * @Date 2022 01 14 21 37
 * @Describe Please describe the role of this class
 **/
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        //设置相应的状态码
        response.setStatus(HttpServletResponse.SC_FORBIDDEN);
        response.setHeader("Content-Type", "application/json;charset=utf-8");
        PrintWriter writer = response.getWriter();
        writer.write("{\"statue\":\"error\",\"msg\":\"权限不足,请联系管理员\"}");
        writer.flush();
        writer.close();
    }
}
3.14.2 Security 的配置类
       //403异常
        http.exceptionHandling()
                .accessDeniedHandler(myAccessDeniedHandler);

3.15 access 自定义权限

3.15.1 MyService
package com.yuan.service;

import org.springframework.security.core.Authentication;

import javax.servlet.http.HttpServletRequest;

/**
 * @Author 小源同学
 * @Date 2022 01 14 22 12
 * @Describe Please describe the role of this class
 **/
public interface MyService {
    boolean hasPermission(HttpServletRequest request, Authentication authentication);
}

3.15.2 MyServiceImpl
package com.yuan.service;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;

import javax.servlet.http.HttpServletRequest;
import java.util.Collection;

/**
 * @Author 小源同学
 * @Date 2022 01 14 22 14
 * @Describe  自定义权限
 **/
@Service
public class MyServiceImpl implements MyService {
    @Override
    public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
        Object principal = authentication.getPrincipal();
        if (principal instanceof UserDetails) {
            UserDetails userDetails = (UserDetails) principal;
            Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities();
            return authorities.contains(new SimpleGrantedAuthority(request.getRequestURI()));
        }
        return false;
    }
}

3.15.3 Security 的配置类
//                .anyRequest().authenticated()
//httpServletRequest会报异常 
/*java.lang.IllegalArgumentException: Failed to evaluate expression @myServiceImpl.hasPermission(httpServletRequest,authentication)'*/
                .anyRequest().access("@myServiceImpl.hasPermission(request,authentication)")

3.16基于注解的访问控制

在Spring Security中提供了一些访问控制的注解。这些注解都是默认是都不可用的,需要通过EnableGlobalMethodsecurity进行开启后使用。
如果设置的条件允许,程序正常执行。如果不允许会报500
org.springframework.security.access.AccessDeniedException:不允许访问
这些注解可以写到Service接口或方法上上也可以写到Controller或Controller的方法上。
通常情况下都是写在控制器方法上的,控制接口URL是否允许被访问。

3.16.1 @Serured 注解

@Secured是专门用于判断是否具有角色的。能写在方法或类上。参数要以ROLE_开头。

@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Secured {

	/**
	 * Returns the list of security configuration attributes (e.g.&nbsp;ROLE_USER,
	 * ROLE_ADMIN).
	 * @return String[] The secure method attributes
	 */
	String[] value();

}
3.16.1.1 Security 的配置类
package com.yuan.config;

import com.yuan.handle.MyAccessDeniedHandler;
import com.yuan.handle.MyAuthenticationFailureHandler;
import com.yuan.handle.MyAuthenticationSuccessHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;

/**
 * @Author 小源同学
 * @Date 2022 01 14 16 17
 * @Describe Security 的配置类
 **/
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private MyAccessDeniedHandler myAccessDeniedHandler;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //自定义登录页面
        http.formLogin()
                /*
                  <input type="text" name="username">
                  <input type="password" name="password">
                  与name相对应
                */
                .usernameParameter("username")
                .passwordParameter("password")
                //当发现、login的时候认为是登录,去执行 UserDetailsServiceImpl->loadUserByUsername
                .loginProcessingUrl("/login")
                //自定义登录页面
                .loginPage("/login.html")
                //登陆成功要跳转的页面,必须是post请求
                .successForwardUrl("/toMain")
//                .successHandler(new MyAuthenticationSuccessHandler("/main.html"))
                //登陆失败要跳转的页面,必须是post请求
                .failureForwardUrl("/toError")
//                .failureHandler(new MyAuthenticationFailureHandler("/error.html"))
        ;

        //设置了自定义登陆页面之后,security提供的原始的认证将全部失效
        //授权认证
        http.authorizeRequests()
                //登录页面,登陆失败页面都不需要认证
                .antMatchers("/login.html", "/error.html").permitAll()
                .antMatchers("/js/**", "/css/**", "/images/**").permitAll()
//                .antMatchers("/main1.html").hasAuthority("admin")
//                .antMatchers("/main1.html").hasAnyAuthority("admin","system")
//                .antMatchers("/main1.html").hasRole("abc")
//                .antMatchers("/main1.html").hasAnyRole("abc","123","chat")
//                .antMatchers("/main1.html").hasIpAddress("127.0.0.1")
                //所以要对所有的请求做拦截做认证【必须是登录之后才能被访问】
                .anyRequest().authenticated()
//                .anyRequest().access("@myServiceImpl.hasPermission(request,authentication)")
        ;

        //403异常
        http.exceptionHandling()
                .accessDeniedHandler(myAccessDeniedHandler);


        //暂时理解为防火墙
        //关闭csrf防护
        http.csrf().disable();
    }

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

3.16.1.2 Controller
    @Secured("ROLE_abc")
    @RequestMapping("/toMain")
    public String toMain() {
        return "redirect:main.html";
    }
3.16.1.3 启动类
@SpringBootApplication
@EnableGlobalMethodSecurity(securedEnabled = true)
public class SpringsecuritydemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringsecuritydemoApplication.class, args);
        System.out.println("启动成功!!!");
    }

}
3.16.2 @PreAuthorize & @PostAuthorize

@PreAuthorize & @PostAuthorize都是方法或者类级别注解

@PreAuthorize表示访问方法或类在执行之前先判断权限,大多情况下都是使用这个注解,注解的参数和access()方法参数取值相同,都是权限表达式。
@PostAuthorize表示方法或类执行结束后判断权限,此注解很少被使用到。

@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface PreAuthorize {

	/**
	 * @return the Spring-EL expression to be evaluated before invoking the protected
	 * method
	 */
	String value();

}
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface PostAuthorize {

	/**
	 * @return the Spring-EL expression to be evaluated after invoking the protected
	 * method
	 */
	String value();

}
3.16.2.1 启动类
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
3.16.2.2 Controller
//    @Secured("ROLE_abc")
//    @PreAuthorize支持以ROLE_开头,配置类不可以
    @PreAuthorize("hasRole('abc')")
    @RequestMapping("/toMain")
    public String toMain() {
        return "redirect:main.html";
    }

3.17 Remember Me 功能实现

Spring Security中 Remember Me 为 记住我 功能,用户只需要在登录时添加remember-me复选框,取值为true。
Spring Security 会自动把用户信息存储到数据源中,以后就可以不登录讲行访问

3.17.1 添加依赖
 		<dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.2.0</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.26</version>
        </dependency> 
3.17.2 Security 的配置类
package com.yuan.config;

import com.yuan.handle.MyAccessDeniedHandler;
import com.yuan.handle.MyAuthenticationFailureHandler;
import com.yuan.handle.MyAuthenticationSuccessHandler;
import com.yuan.service.UserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationException;
import org.springframework.stereotype.Component;

import javax.sql.DataSource;

/**
 * @Author 小源同学
 * @Date 2022 01 14 16 17
 * @Describe Security 的配置类
 **/
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private MyAccessDeniedHandler myAccessDeniedHandler;
    @Autowired
    private UserDetailsServiceImpl userDetailsService;
    @Autowired
    private DataSource dataSource;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //自定义登录页面
        http.formLogin()
                /*
                  <input type="text" name="username">
                  <input type="password" name="password">
                  与name相对应
                */
                .usernameParameter("username")
                .passwordParameter("password")
                //当发现、login的时候认为是登录,去执行 UserDetailsServiceImpl->loadUserByUsername
                .loginProcessingUrl("/login")
                //自定义登录页面
                .loginPage("/login.html")
                //登陆成功要跳转的页面,必须是post请求
                .successForwardUrl("/toMain")
//                .successHandler(new MyAuthenticationSuccessHandler("/main.html"))
                //登陆失败要跳转的页面,必须是post请求
                .failureForwardUrl("/toError")
//                .failureHandler(new MyAuthenticationFailureHandler("/error.html"))
        ;

        //设置了自定义登陆页面之后,security提供的原始的认证将全部失效
        //授权认证
        http.authorizeRequests()
                //登录页面,登陆失败页面都不需要认证
                .antMatchers("/login.html", "/error.html").permitAll()
                .antMatchers("/js/**", "/css/**", "/images/**").permitAll()
//                .antMatchers("/main1.html").hasAuthority("admin")
//                .antMatchers("/main1.html").hasAnyAuthority("admin","system")
//                .antMatchers("/main1.html").hasRole("abc")
//                .antMatchers("/main1.html").hasAnyRole("abc","123","chat")
//                .antMatchers("/main1.html").hasIpAddress("127.0.0.1")
                //所以要对所有的请求做拦截做认证【必须是登录之后才能被访问】
                .anyRequest().authenticated()
//                .anyRequest().access("@myServiceImpl.hasPermission(request,authentication)")
        ;

        //403异常
        http.exceptionHandling()
                .accessDeniedHandler(myAccessDeniedHandler);
        //RememberMe功能实现
        http.rememberMe()
                //自定义登录逻辑
                .userDetailsService(userDetailsService)
                .tokenValiditySeconds(20) //1小时 记住我的时间(秒),默认是14天
                //持久层对象
                .tokenRepository(getPersistentTokenRepository());

        //暂时理解为防火墙
        //关闭csrf防护
        http.csrf().disable();
    }


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


    public PersistentTokenRepository getPersistentTokenRepository() {
        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
        jdbcTokenRepository.setDataSource(dataSource);
        //自动建表,第一次启动会创建,再次起订记得关闭
//        jdbcTokenRepository.setCreateTableOnStartup(true);
        return jdbcTokenRepository;
    }
}
3.17.3 application.yml
spring:
  datasource:
    username: root
    password: 123456
    url: jdbc:mysql://localhost:3306/security?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8
    driver-class-name: com.mysql.cj.jdbc.Driver

3.18 Thymeleaf 在 SpringSecurity 中的使用

Spring Security可以在一些视图技术中进行控制显示效果。例如:JSP 或 Thymeleaf。在非前后端分离且使用 Spring Boot 的项目中多使用 Thymeleaf 作为视图展示技术。
Thymeleaf 对Spring Security 的支持都放在 thymeleaf-extras-springsecurityx 中,目前最新版本为5。所以需要在项目中添加此 jar 包的依赖和thymeleaf 的依赖

3.18.1 添加依赖
        <dependency>
            <groupId>org.thymeleaf.extras</groupId>
            <artifactId>thymeleaf-extras-springsecurity5</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
3.18.2 thymeleaf 命名空间和 security 命名空间
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
3.18.3 demo.html
<! DOCTYPE htm1>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
<head>
    <meta charset="UTF-8">
    <title>thymeleaf-demo</title>
</head>
<body>
登录账号:<span sec:authentication="name">123</span><br/>
登录账号:<span sec:authentication="principal. username">456</span><br/>
凭证:<span sec:authentication="credentials">456</span><br/>
权限和角色:<span sec:authentication="authorities">456</span><br/>
客户端地址:<span sec:authentication="details. remoteAddress">456</span><br/>
sessionld:<span sec:authentication="detai1s. sessionld">456</span><br/>
</body>
</html>
3.18.4 Controller
    @RequestMapping("/demo")
    public String demo() {
        return "demo";
    }

3.19 在 thymeleaf 中进行权限判断

3.19.1 demo.html
通过权限判断:
<button sec:authorize="hasAuthority('/insert')">新增</button>
<button sec:authorize="hasAuthority('/delete')">删除</button>
<button sec:authorize="hasAuthority('/update')">修改</button>
<button sec:authorize="hasAuthority('/select')">查看</button>
<br/>
通过角色判断:
<button sec:authorize="hasRole('abc')">新增</button>
<button sec:authorize="hasRole('abc')">删除</button>
<button sec:authorize="hasRole('abc')">修改</button>
<button sec:authorize="hasRole('abC')">查看</button>

3.20 logout 退出登录的操作

3.20.1 Security 的配置类
        //退出登录
        http.logout()
//                .logoutUrl("/user/logout")
                .logoutUrl("/logout")
                //退出成功跳转的页面
                .logoutSuccessUrl("/login.html");
3.20.2 main.html
<a href="/logout">退出</a>

3.21 Spring Security 中的 csrf

CSRF (Cross-site request forgery))跨站请求伪造,也被称为 "OneClick Attack"或者Session Riding。通过伪造用户请求访问受信任站点的非法请求访问。
跨域:只要网络协议,ip地址,端口中狂何一个不相同就是跨域请求。
客户端服务进行交互时,由于http 协议本身是无状态协议,所以引入了cookie进行记录客户端身份。在cookie中会存放session id用来识别客户端身份的。在跨域的情况下,session id可能被第三方恶意劫持,通过这个session id向服务端发起请求时,服务端会认为这个请求是合法的,可能发生很多意想不到的事情。

从Spring Security4开始CSRF防护默认开启。默认会拦截请求。进行CSRF处理。CSRF为了保证不是其他第三方网站访问,要求访问时携带参数名为_csrf值为token(token 在服务端产生)的内容,如果token和服务端的token匹配成功,则正常访问。

3.21.1 Controller
    @RequestMapping("/showLogin")
    public String showLogin() {
        return "login";
    }

3.21.2 Security 配置类
   
//1. 把csrf().disable();注释了
//  http.csrf().disable();
//2. 放行/showLogin
   .antMatchers("/showLogin", "/logout","/error.html").permitAll()
//3. 修改自定义登录页面
   .loginPage("/showLogin")

4. Oauth2 认证

第三方认证技术方案最主要是解决认证协议的通用标准问题,因为要实现跨系统认证,各系统之间要遵循一定的接口协议。
OAUTH协议为用户资源的授权提供了一个安全的、开放而又简易的标准。同时,任何第三方都可以使用OAUTH认证服务,任何服务提供商都可以实现自身的OAUTH认证服务,因而OAUTH是开放的。业界提供了OAUTH的多种实现如PHP、JavaScript,Java,Ruby等各种语言开发包,大大节约了程序员的时间,因而OAUTH是简易的。互联网很多服务如Open API,很多大公司如Google,Yahoo,Microsoft等都提供了OAUTH认证服务,这些都足以说明OAUTH标准逐渐成为开放资源授权的标准。
Oauth协议目前发展到2.0版本,1.0版本过于复杂,2.0版本已得到广泛应用。

  1. 用户进入网站的登录页面,点击微信的图标以微信账号登录系统,用户是自己在微信里信息的资源拥有者。

  2. 点击“微信""出现一个二维码,此时用户扫描二维码,开始给网站授权。

  3. 资源拥有者同意给客户端授权

    资源拥有者扫描二维码表示资源拥有者同意给客户端授权,微信会对资源拥有者的身份进行验证,验证通过后,微信会询问用户是否给授权网站访问自己的微信数据,用户点击“确认登录"表示同意授权,微信认证服务器会颁发一个授权码,并重定向式网站。

  4. 客户端获取到授权码,请求认证服务器申请令牌

    此过程用户看不到,客户端应用程序请求认证服务器,请求携带授权码。

  5. 认证服务器向客户端响应令牌

    认证服务器验证了客户端请求的授权码,如果合法则给客户端颁发令牌,令牌是客户端访问资源的通行证。此交互过程用户看不到,当客户端拿到令牌后,用户在网站看到已经登录成功。

  6. 客户端请求资源服务器的资源

    客户端携带令牌访问资源服务器的资源。网站携带令牌请求访问微信服务器获取用户的基本信息。6.资源服务器返回受保护资源资源服务器校验令牌的合法性,如果合法则向用户响应资源信息内容。

注意:资源服务器和认证服务器可以是一个服务也可以分开的服务,如果是分开的服务资源服务器通常要请求认证服务器来校验令牌的合法性。

OAuth 2.0的运行流程如下图,摘自RFC 6749。

img

(A)用户打开客户端以后,客户端要求用户给予授权。

(B)用户同意给予客户端授权。

(C)客户端使用上一步获得的授权,向认证服务器申请令牌。

(D)认证服务器对客户端进行认证以后,确认无误,同意发放令牌。

(E)客户端使用令牌,向资源服务器申请获取资源。

(F)资源服务器确认令牌无误,同意向客户端开放资源。

不难看出来,上面六个步骤之中,B是关键,即用户怎样才能给于客户端授权。有了这个授权以后,客户端就可以获取令牌,进而凭令牌获取资源。

4.1 常用术语

客户凭证(c1ient Credentials):客户端的clientld和密码用于认证客户

令牌(tokens):授权服务器在接收到客户请求后,颁发的访问令牌

作用域(scopes):客户请求访问令牌时,由资源拥有者额外指定的细分权限(permission)

4.2 令牌类型

授权码:仅用于授权码授权类型,用于交换获取访问令牌和刷新令牌

访问令牌:用于代表一个用户或服务直接去访问受保护的资源

刷新令牌:用于去授权服务器获取一个刷新访问令牌

BearerToken:不管谁拿到Token都可以访问资源,类似现金

Proof of Possession(PoP) Token:可以校验client是否对Token有明确的拥有权

4.3 特点

4.3.1 优点

更安全,客户端不接触用户密码,服务器端更易集中保护

广泛传播并被持续采用

短寿命和封装的token

资源服务器和授权服务器解耦

集中式授权,简化客户端

HTTP/JSON友好,易于请求和传递token

考虑多种客户端架构场景

客户可以具有不同的信任级别

4.3.2 缺点

协议框架太宽泛,造成各种实现的兼容性和互操作性差

不是一个认证协议,本身并不能告诉你任何用户信息

4.4 授权模式

4.4.1 授权码模式

img

(A)用户访问客户端,后者将前者导向认证服务器。

(B)用户选择是否给予客户端授权。

(C)假设用户给予授权,认证服务器将用户导向客户端事先指定的"重定向URI"(redirection URI),同时附上一个授权码。

(D)客户端收到授权码,附上早先的"重定向URI",向认证服务器申请令牌。这一步是在客户端的后台的服务器上完成的,对用户不可见。

(E)认证服务器核对了授权码和重定向URI,确认无误后,向客户端发送访问令牌(access token)和更新令牌(refresh token)。

4.4.2 简化模式

img

(A)客户端将用户导向认证服务器。

(B)用户决定是否给于客户端授权。

(C)假设用户给予授权,认证服务器将用户导向客户端指定的"重定向URI",并在URI的Hash部分包含了访问令牌。

(D)浏览器向资源服务器发出请求,其中不包括上一步收到的Hash值。

(E)资源服务器返回一个网页,其中包含的代码可以获取Hash值中的令牌。

(F)浏览器执行上一步获得的脚本,提取出令牌。

(G)浏览器将令牌发给客户端。

4.4.3 密码模式

img

(A)用户向客户端提供用户名和密码。

(B)客户端将用户名和密码发给认证服务器,向后者请求令牌。

(C)认证服务器确认无误后,向客户端提供访问令牌。

4.4.4 客户端模式

img

(A)客户端向认证服务器进行身份认证,并要求一个访问令牌。

(B)认证服务器确认无误后,向客户端提供访问令牌。

4.4.5 更新令牌

image-20220115171757315

5. Spring Security Oauth2

5.1 添加依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.2</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <groupId>com.yuan</groupId>
    <artifactId>springsecurityoauth2</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>springsecurityoauth2</name>
    <description>springsecurityoauth2</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <!-- security-oauth2自动装配依赖 -->
        <dependency>
            <groupId>org.springframework.security.oauth.boot</groupId>
            <artifactId>spring-security-oauth2-autoconfigure</artifactId>
        </dependency>
        <!-- security starter依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!--对象池-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Greenwich.SR6</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>2.6.2</version>
            </plugin>
        </plugins>
    </build>

</project>

5.2 授权码模式

5.2.1 Security 配置类
package com.yuan.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; 
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
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;

/**
 * @Author 小源同学
 * @Date 2022 01 15 19 22
 * @Describe Security 配置类
 **/
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

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

5.2.2 自定登录逻辑
package com.yuan.service;

import com.yuan.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

/**
 * @Author 小源同学
 * @Date 2022 01 15 19 21
 * @Describe  自定登录逻辑
 **/
@Service
public class UserService implements UserDetailsService {
    @Autowired
    private PasswordEncoder passwordEncoder;
    
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        String password = passwordEncoder.encode("123");
        return new User("admin", password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
    }
}

5.2.3 自定义 User

因为一些原因,is***()的方法改成了true,如果启动登录没有问题[锁定,失效 …],可以不改动【false】

package com.yuan.pojo;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.List;

/**
 * @Author 小源同学
 * @Date 2022 01 15 19 24
 * @Describe  自定义 User
 **/
public class User implements UserDetails {
    private String username;
    private String password;
    private List<GrantedAuthority> authorities;

    public User(String username, String password, List<GrantedAuthority> authorities) {
        this.username = username;
        this.password = password;
        this.authorities = authorities;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return username;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

5.2.4 授权服务器配置
package com.yuan.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;

/**
 * @Author 小源同学
 * @Date 2022 01 15 19 34
 * @Describe 授权服务器配置
 **/
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                //配置client-id
                .withClient("admin")
                //配置client-secret
                .secret(passwordEncoder.encode("112233"))
                //配置访问token的有效期
//                .accessTokenValiditySeconds(60 * 60)
                //配置redirect-utl,用于授权成功后跳转
                .redirectUris("http://www.baidu.com")
                //配置申请的权限范围
                .scopes("all")
                //配置grant-type,授权类型,【授权码模式】
                .authorizedGrantTypes("authorization_code")
                ;
    }
}

5.2.5 资源服务器配置
package com.yuan.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;

/**
 * @Author 小源同学
 * @Date 2022 01 15 19 42
 * @Describe 资源服务器配置
 **/
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest()
                .authenticated()
                .and()
                .requestMatchers()
                .antMatchers("/user/**");

    }
}

5.2.6 启动测试
5.2.6.1 地址栏输入
http://localhost:8080/oauth/authorize?response_type=code&client_id=admin&redirect_url=http://www.baidu.com&scope=all

image-20220115225123271

说明已经正确启动了。

选择Approve -> Authorize 在地址栏会有code=xsbt20这个就是授权码

5.2.6.2 打开Postman
http://localhost:8080/oauth/token

image-20220115231205321

image-20220115231150296

我们就得到了 access_tokentoken_typeexpires_inscope

image-20220115231224426

访问我们写的Controller

http://localhost:8080/user/getCurrentUser

image-20220115231656828

5.3 密码模式

5.3.1 Security 配置类
    @Bean
    public AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManager();
    }
5.3.2 授权服务器配置
package com.yuan.config;

import com.yuan.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;

/**
 * @Author 小源同学
 * @Date 2022 01 15 19 34
 * @Describe 授权服务器配置
 **/
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
    @Autowired
    private PasswordEncoder passwordEncoder;
    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private UserService userService;

    /**
     * 使用密码模式的配置
     * @param endpoints
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.authenticationManager(authenticationManager)
                .userDetailsService(userService);
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                //配置client-id
                .withClient("admin")
                //配置client-secret
                .secret(passwordEncoder.encode("112233"))
                //配置访问token的有效期
//                .accessTokenValiditySeconds(60 * 60)
                //配置redirect-utl,用于授权成功后跳转
                .redirectUris("http://www.baidu.com")
                //配置申请的权限范围
                .scopes("all")
                //配置grant-type,授权类型,【授权码模式】
//                .authorizedGrantTypes("authorization_code")
                //配置grant-type,授权类型,【密码模式】
                .authorizedGrantTypes("password")
        ;
    }
}

5.3.3 启动测试
http://localhost:8080/oauth/token

image-20220115232712233

访问我们写的Controller

http://localhost:8080/user/getCurrentUser

image-20220115232810643

5.4 Readis 存储 Token

5.4.1 Redis 配置类
package com.yuan.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;

/**
 * @Author 小源同学
 * @Date 2022 01 16 00 26
 * @Describe Redis 配置类
 **/
@Configuration
public class RedisConfig {
    @Autowired
    private RedisConnectionFactory redisConnectionFactory;

    @Bean
    public TokenStore redisTokenStore(){
        return new RedisTokenStore(redisConnectionFactory);
    }
}
5.4.2 授权服务器配置
    @Autowired
    @Qualifier("redisTokenStore")
    private TokenStore tokenStore; 

	@Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.authenticationManager(authenticationManager)
                .userDetailsService(userService)
                .tokenStore(tokenStore);
    }
5.4.3 启动测试
http://localhost:8080/oauth/token

image-20220116003757124

成功储存在redis里

image-20220116003956445

6. JWT

6.1 常见的认证机制

6.1.1 HTTP Basic Auth

HTTP Basic Auth简单点说明就是每次请求API时都提供用户的username和password,简言之,Basic Auth是配合RESTful API使用的最简单的认证方式,只

需提供用户名密码即可,但由于有把用户名密码暴露给第三方客户端的风险,在生产环境下被使用的越来越少。因此,在开发对外开放的RESTful API时,尽量避

免采用HTTP BasicAuth。

6.1.2 Cookie Auth

Cookie认证机制就是为一次请求认证在服务端创建一个Session对象,同时在客户端的浏览器端创建了一个Cookie对象;通过客户端带上来Cookie对象来与服务

器端的session对象匹配来实现状态管理的。默认的,当我们关闭浏览器的时候,cookie会被删除。但可以通过修改cookie的expire time使cookie在一定时间

内有效。

6.1.3 Token Auth

使用基于Token的身份验证方法,在服务端不需要存储用户的登录记录。

6.1.3.1 Token Auth 大概的流程
  1. 客户端使用用户名跟密码请求登录
  2. 服务端收到请求,去验证用户名与密码
  3. 验证成功后,服务端会签发一个Token,再把这个Token发送给客户端4.客户端收到Token 以后可以把它存储起来,比如放在Cookie里
  4. 客户端每次向服务端请求资源的时候需要带着服务端签发的Token
  5. 服务端收到请求,然后去验证客户端请求里面带着的Token,如果验证成功,就向客户端返回请求的数据

比 HTTP Basic Auth 方式更安全,比 Cookie Auth 方式更节约服务器资源,比 OAuth 方式更加轻量。

6.1.3.2 Token Auth 的优点:
  1. 支持跨域访问

Cookie是不允许垮域访问的,这一点对Token机制是不存在的,前提是传输的用户认证信息通过HTTP头传输.

  1. 无状态(服务端可扩展行)

Token机制在服务端不需要存储session信息,因为Token 自身包含了所有登录用户的信息,只需要在客户端的cookie或本地介质存储状态信息.

  1. 更适用CDN

可以通过内容分发网络请求你服务端的所有资料(如:javascript,HTML,图片等),而你的服务端只要提供API即可.

  1. 去耦

不需要绑定到一个特定的身份验证方案。Token可以在任何地方生成,只要在你的API被调用的时候,你可以进行Token生成调用即可.

  1. 更适用于移动应用

当你的客户端是一个原生平台(iOS, Android,Windows 10等)时,Cookie是不被支持的(你需要通过Cookie容器进行处理),这时采用Token认证机制就会

简单得多。

  1. CSRF(跨站请求伪造Cross-site request forgery)

因为不再依赖于Cookie,所以你就不需要考虑对CSRF(跨站请求伪造)的防范。

  1. 性能

一次网络往返时间(通过数据库查询session信息)总比做一次HMACSHA256计算 的Token验证和解析要费时得多.

  1. 不需要为登录页面做特殊处理

如果你使用Protractor 做功能测试的时候,不再需要为登录页面做特殊处理.

  1. 基于标准化

你的API可以采用标准化的 JSON Web Token (JWT). 这个标准已经存在多个后端库(.NET, Ruby, Java,Python, PHP)和多家公司的支持(如:

Firebase,Google, Microsoft)

img

6.1.3 OAuth

OAuth(开放授权)是一个开放的授权标准,允许用户让第三方应用访问该用户在某一web服务上存储的私密的资源(如照片,视频,联系人列表),而无需将用

户名和密码提供给第三方应用。

OAuth允许用户提供一个令牌,而不是用户名和密码来访问他们存放在特定服务提供者的数据。每一个令牌授权一个特定的第三方

系统(例如,视频编辑网站)在特定的时段(例如,接下来的2小时内)内访问特定的资源(例如仅仅是某一相册中的视频)。这样,OAuth让用户可以授权第三方

网站访问他们存储在另外服务提供者的某些特定信息,而非所有内容

下面是OAuth2.0的流程:

img

这种基于OAuth的认证机制适用于个人消费者类的互联网产品,如社交类APP等应用,但是不太适合拥有自有认证权限管理的企业应用。

6.2 JWT 简介

Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的,特别适用于分

布式站点的单点登录(SSO)场景。

JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声

明信息,该token也可直接被用于认证,也可被加密。

6.2.1 JWT令牌的优点
  1. jwt基于json,非常方便解析。
  2. 可以在令牌中自定义丰富的内容,易扩展。
  3. 通过非对称加密算法及数字签名技术,JWT防止篡改,安全性高。4.资源服务使用JWT可不依赖认证服务即可完成授权。
6.2.2 JWT令牌的缺点
  1. JWT令牌较长,占存储空间比较大。

6.3 JWT的构成

第一部分我们称它为头部(Header),第二部分我们称其为载荷(Payload, 类似于飞机上承载的物品),第三部分是签证(Signature).

6.3.1 头部(Header)

jwt的头部承载两部分信息:

  • 声明类型,这里是jwt
  • 声明加密的算法 通常直接使用 HMAC HS256

完整的头部就像下面这样的JSON:

{
  'typ': 'JWT',
  'alg': 'HS256'
}

然后将头部进行base64加密(该加密是可以对称解密的),构成了第一部分

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
6.3.2 载荷(Payload, 类似于飞机上承载的物品)
  1. 标准中注册的声明(建议但不强制使用)
iss (issuer):签发人
exp (expiration time):过期时间
sub (subject):主题
aud (audience):受众
nbf (Not Before):生效时间
iat (Issued At):签发时间
jti (JWT ID):编号
  1. 公共的声明

公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.

  1. 私有的声明

私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。

这个指的就是自定义的claim。比如下面那个举例中的name都属于自定的claim。这些claim跟WT标准规定的claim区别在于:JWT规定的claim,JWT的接收方

在拿到WT之后,都知道怎么对这些标准的claim进行验证(还不知道是否能够验证);而private claims不会验证,除非明确告诉接收方要对这些claim进行验

证以及规则才行。

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}
其中sub是标准的声明,name是自定义的声明(公共的或私有的)

然后将其进行base64加密,得到Jwt的第二部分

eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
6.3.2 签证(Signature)、签名

jwt的第三部分是一个签证信息,这个签证信息由三部分组成:

  • header (base64后的)
  • payload (base64后的)
  • secret

这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串(头部在前),然后通过header中声明的加密方式进行加盐secret组合

加密,然后就构成了jwt的第三部分:

UQmqAUhUrpDVV2ST7mZKyLTomVfg7sYkEjmdDI5XF8Q

注意: secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场

景都不应该流露出去。一旦客户端得知这个secret,那就意味着客户端是可以自我签发jwt了。

6.3.2.1 签名的目的

最后一步签名的过程,实际上是对头部以及载荷内容进行签名。一般而言,加密算法对于不同的输入产生的输出总是不一样的。对于两个不同的输入,产生同样

的输出的概率极其地小(有可能比我成世界首富的概率还小)。所以,我们就把“不一样的输入产生不一样的输出”当做必然事件来看待吧。

所以,如果有人对头部以及载荷的内容解码之后进行修改,再进行编码的话,那么新的头部和载荷的签名和之前的签名就将是不一样的。而且,如果不知道服

务器加密的时候用的密钥的话,得出来的签名也一定会是不一样的。

服务器应用在接受到JWT后,会首先对头部和载荷的内容用同一算法再次签名。那么服务器应用是怎么知道我们用的是哪一种算法呢?别忘了,我们在JWT的头部

中已经用alg字段指明了我们的加密算法了。

如果服务器应用对头部和载荷再次以同样方法签名之后发现,自己计算出来的签名和接受到的签名不一样,那么就说明这个Token的内容被别人动过的,我们

应该拒绝这个Token,返回一个HTTP 401 Unauthorized响应。

注意:在JWT中,不应该在载荷里面加入任何敏感的数据,比如用户的密码。

6.4 JJWT

6.4.1 添加依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.2</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.yuan</groupId>
    <artifactId>jjwtdemo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>jjwtdemo</name>
    <description>jjwtdemo</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>2.6.2</version>
            </plugin>
        </plugins>
    </build>

</project>

6.4.2 生成令牌
 @Test
    public void testCreateToken() {
        JwtBuilder jwtBuilder = Jwts.builder()
                //声明标识("jti":”8888“)
                .setId("8888")
                //主题("sub":"xiaoyuan")
                .setSubject("xiaoyuan")
                //创建日期("ita":"*******")
                .setIssuedAt(new Date())
                //设置过期时间  10分钟
                .setExpiration(new Date(System.currentTimeMillis() + 60 * 10000))
                //设置签名
                .signWith(SignatureAlgorithm.HS256, "yuan")
                //自定义申明
                .claim("roles","admin")
                .claim("logo","***.jpg")
                //可以直接传入map
//                .addClaims(map)
                ;
        String token = jwtBuilder.compact();
        System.out.println(token);
        System.out.println("===========================================");
        String[] split = token.split("\\.");
        for (String s : split) System.out.println(Base64Codec.BASE64.decodeToString(s));
    }
6.4.3 解析令牌
@Test
    public void testParseToken() {
        String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODg4Iiwic3ViIjoieGlhb3l1YW4iLCJpYXQiOjE2NDIzMDkzMTQsImV4cCI6MTY0MjMwOTkxNCwicm9sZXMiOiJhZG1pbiIsImxvZ28iOiIqKiouanBnIn0.vECoGRZ26pwmEeeKxPwfrTLzf7wQYOYSWqWv6oDohZE";

        Claims claims = Jwts.parser().setSigningKey("yuan").parseClaimsJws(token).getBody();

        System.out.println("id:" + claims.getId());
        System.out.println("sub:" + claims.getSubject());
        System.out.println("sub:" + claims.getSubject());
        System.out.println("roles:" + claims.get("roles"));
        System.out.println("logo:" + claims.get("logo"));
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        System.out.println("签发时间:" + simpleDateFormat.format(claims.getIssuedAt()));
        System.out.println("过期时间:" + simpleDateFormat.format(claims.getExpiration()));
        System.out.println("当前时间:" + simpleDateFormat.format(new Date())); 
    }

7.Spring Security Oauth2 整合 JWT

7.1 JwtToken 配置类

package com.yuan.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;

/**
 * @Author 小源同学
 * @Date 2022 01 16 13 07
 * @Describe JwtToken 配置类
 **/
@Configuration
public class JwtTokenStoreConfig {

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

    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
        jwtAccessTokenConverter.setSigningKey("TEST_KEY");
        return jwtAccessTokenConverter;
    } 
}

7.2 授权服务器配置

    @Autowired
    @Qualifier("jwtTokenStore")
    private TokenStore tokenStore;
    @Autowired
    private JwtAccessTokenConverter jwtAccessTokenConverter;
    
 	@Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.authenticationManager(authenticationManager)
                .userDetailsService(userService)
                //配置令牌存储策略
                .tokenStore(tokenStore)
                .accessTokenConverter(jwtAccessTokenConverter)
        ;
    }

7.3 扩展 JWT 里的内容存储

7.3.1 Jwt内容增强器
package com.yuan.config;

import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;

import java.util.HashMap;
import java.util.Map;

/**
 * @Author 小源同学
 * @Date 2022 01 16 13 23
 * @Describe Jwt内容增强器
 **/
public class JwtTokenEnhancer implements TokenEnhancer {
    @Override
    public OAuth2AccessToken enhance(OAuth2AccessToken oAuth2AccessToken, OAuth2Authentication oAuth2Authentication) {
        Map<String, Object> map = new HashMap<>();
        map.put("enhancer", "Enhancer Map");
        ((DefaultOAuth2AccessToken) oAuth2AccessToken).setAdditionalInformation(map);
        return oAuth2AccessToken;
    }
}

7.3.2 JwtToken 配置类
    @Bean
    public JwtTokenEnhancer jwtTokenEnhancer() {
        return new JwtTokenEnhancer();
    }
7.3.3 授权服务器配置
    @Autowired
    private JwtTokenEnhancer jwtTokenEnhancer;
    
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        //配置Jwt内容增强器
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        List<TokenEnhancer> delegates = new ArrayList<>();
        delegates.add(jwtTokenEnhancer);
        delegates.add(jwtAccessTokenConverter);
        tokenEnhancerChain.setTokenEnhancers(delegates);

        endpoints.authenticationManager(authenticationManager)
                .userDetailsService(userService)
                //配置令牌存储策略
                .tokenStore(tokenStore)
                .accessTokenConverter(jwtAccessTokenConverter)
                .tokenEnhancer(tokenEnhancerChain)
        ;
    }

7.4 解析 Jwt 中的内容

7.4.1 添加依赖
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.0</version>
</dependency>
7.4.2 Controller
    /**
     * 获取 JwtToken 解析的信息
     *
     * @param authentication
     * @return
     */
    @RequestMapping("/getJwtToken")
    public Object getCurrentUser(Authentication authentication, HttpServletRequest httpServletRequest) {
        String authorization = httpServletRequest.getHeader("Authorization");
        System.out.println(authorization);
        String token = authorization.substring(authorization.indexOf("bearer") + 7);
        return Jwts.parser().setSigningKey("TEST_KEY".getBytes(StandardCharsets.UTF_8)).parseClaimsJws(token).getBody();
    }

7.5 刷新令牌

7.5.1 授权服务器
  .authorizedGrantTypes("password","authorization_code","refresh_token")

image-20220116140046651

8. Spring Security Oauth2 整合 SSO

8.1 添加依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.2</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <groupId>com.yuan</groupId>
    <artifactId>oauth2client01</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>oauth2client01</name>
    <description>oauth2client01</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>


    <dependencies>
        <!-- security-oauth2自动装配依赖 -->
        <dependency>
            <groupId>org.springframework.security.oauth.boot</groupId>
            <artifactId>spring-security-oauth2-autoconfigure</artifactId>
        </dependency>
        <!-- security starter依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.0</version>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Greenwich.SR6</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

8.2 启动类

@SpringBootApplication
//开启单点登录
@EnableOAuth2Sso
public class Oauth2client01Application {
    public static void main(String[] args) {
        SpringApplication.run(Oauth2client01Application.class, args);
    }
}

8.3 Client01 的 Controller

package com.yuan.controller;

import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @Author 小源同学
 * @Date 2022 01 16 17 10
 * @Describe Please describe the role of this class
 **/
@RestController
@RequestMapping(("/user"))
public class UserController {
    /**
     * 获取当前用户信息
     * @param authentication
     * @return
     */
    @RequestMapping("/getCurrentUser")
    public Object getCurrentUser(Authentication authentication) {
        return authentication;
    }
}

8.4 application.yml

server:
  port: 8081
  servlet:
    session:
      cookie:
        #        防止Cookie冲突,冲突导致验证不通过
        name: OAUTH2-CLIENT-SESSIONID01
#授权服务器地址
oauth2-server-url: http://localhost:8080
#与授权服务器对应的地址
security:
  oauth2:
    client:
      client-id: admin
      client-secret: 112233
      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

8.5 原来的 Spring Security Oauth2 的授权服务器配置

package com.yuan.config;

import com.yuan.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.web.servlet.oauth2.resourceserver.OAuth2ResourceServerSecurityMarker;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;
import org.springframework.security.oauth2.provider.token.TokenEnhancerChain;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;

import java.util.ArrayList;
import java.util.List;

/**
 * @Author 小源同学
 * @Date 2022 01 15 19 34
 * @Describe 授权服务器配置
 **/
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
    @Autowired
    private PasswordEncoder passwordEncoder;
    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private UserService userService;
    /*  @Autowired
        @Qualifier("redisTokenStore")
        private TokenStore tokenStore;*/
    @Autowired
    @Qualifier("jwtTokenStore")
    private TokenStore tokenStore;
    @Autowired
    private JwtAccessTokenConverter jwtAccessTokenConverter;
    @Autowired
    private JwtTokenEnhancer jwtTokenEnhancer;

    /**
     * 使用密码模式的配置
     *
     * @param endpoints
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        //配置Jwt内容增强器
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        List<TokenEnhancer> delegates = new ArrayList<>();
        delegates.add(jwtTokenEnhancer);
        delegates.add(jwtAccessTokenConverter);
        tokenEnhancerChain.setTokenEnhancers(delegates);

        endpoints.authenticationManager(authenticationManager)
                .userDetailsService(userService)
                //配置令牌存储策略
                .tokenStore(tokenStore)
                .accessTokenConverter(jwtAccessTokenConverter)
                .tokenEnhancer(tokenEnhancerChain)
        ;
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                //配置client-id
                .withClient("admin")
                //配置client-secret
                .secret(passwordEncoder.encode("112233"))
                //配置访问token的有效期
                .accessTokenValiditySeconds(60 * 600)
                //配置刷新令牌的有效期
                .refreshTokenValiditySeconds(864000)
                //配置redirect-utl,用于授权成功后跳转
                .redirectUris("http://localhost:8081/login")
                //设置自动授权
                .autoApprove(true)
                //配置申请的权限范围
                .scopes("all")
                //配置grant-type,授权类型,【授权码模式】
//                .authorizedGrantTypes("authorization_code")
                //配置grant-type,授权类型,【密码模式】
                .authorizedGrantTypes("password", "authorization_code", "refresh_token")
        ;
    }

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

8.5 操作演示

8.5.1 访问api

客户端client01的api

http://localhost:8081/user/getCurrentUser

跳转到了服务器上 Security 提供的登陆界面

http://localhost:8080/login

输入在服务器上配置的账号密码(注意:这里再授权服务器里配置了自动授权

再次回到了client01的api访问接口上

http://localhost:8081/user/getCurrentUser

显示出了当前登录用户的信息

image-20220116173946003

;