SpringSecurity快速入门

SpringSecurity快速入门

一、简介

利用springIOC(控制反转)、AOP、DI(依赖注入),为项目设置安全的认证、授权、会话管理、密码加密等技术,减少权限代码量,增加可移植性。

二、快速入门

  • 导入依赖
 <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>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional>
        </dependency>
  • 前端登录页面(放在/resources/static/中)
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登录页</title>
</head>
<body>
<!--name值必须为username和password-->
   <form action="login" method="post">
       用户名:<input type="text" name="username"><br>
       密码:<input type="password" name="password"><br>
         <input type="checkbox" value="true" name="remember"> 记住我
       <button type="submit">登录</button>
   </form>
</body>
</html>
  • 配置文件
server:
  port: 9090
  servlet:
    context-path: /security
    #前缀使用默认: /static/ /public/ /resource/

登录用户名:user 密码在控制台随机生成,每次不一样,登录成功后跳转到自定义登录页面。

三、配置业务逻辑层

  • 配置处理登录用户名和密码的类
package com.zwf.spring_security.service;

import org.springframework.boot.autoconfigure.security.SecurityProperties;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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.security.web.authentication.switchuser.SwitchUserGrantedAuthority;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;

/**
 * @author Mr Zeng
 * @version 1.0
 * @date 2023-09-29 10:40
 */
@Service
public class UserDetailService implements UserDetailsService {

    @Resource
    private PasswordEncoder getPwd;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //获取前台的username值
        if(!"admin".equals(username)){
            throw new UsernameNotFoundException("用户名不存在!");
        }
          //配置角色名的时候要加上ROLE_前缀  角色名和资源名严格区分大小写
         //可配置多个用逗号分割。
        List<GrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_admin,normal,ROLE_root,ROLE_sys,/security/successLogin,/security/main.html");
        //构造参数有用户名 密码  权限名(不为空)
        User user = new User(username,getPwd.encode("123456"),authorities);
        return user;
    }
}

配置核心配置类(里面配置授权和认证)

@EnableWebSecurity
public class PermissionConfig {
    @Resource
    private AccessDeniedHandler denyAccessExceptionDemo;
     @Resource
    private PersistentTokenRepository getPersistent;

     @Resource
     private UserDetailService userDetailService;


    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.authorizeRequests((auth)->{
             auth.antMatchers("/login.html").permitAll()
                            .antMatchers("/error.html").permitAll()
                             //静态资源被放行 ant表达式:?表示一个字符 * 表示0个或者多个字符
                              // **表示多级目录 HttpMethod.GET表示不认证get请求的页面
                            .antMatchers(HttpMethod.GET,"/js/**","/img/**","/css/**").permitAll()
                                 //不是简单的多层路径 而是在servlet中配置的<url-patten>/authencation</url-patten>
                               //需要在yml文件中配置  spring.mvc.servlet.path=/authencation  表示/authencation/login放行
                                //只有mvcMatchers()才有servletPath()这个方法
//                              .mvcMatchers("/login").servletPath("/authencation").permitAll()
                                 //自定义访问类
                             //用正则表达式定义文件路径 去匹配
//                             .regexMatchers(".+[.]js").permitAll()
                             //配置只有admin角色才能访问 这里的角色名不能加前缀 ROLE_ 严格区分大小写
//                             .antMatchers("/make.html").hasRole("admin")
                              //配置多个角色  只要有一个角色满足就可以访问
//                             .antMatchers("/make.html").hasAnyRole("admin","root","sys")
                                //只有拥有normal资源的才能访问  hasAnyAuthority()配置多个资源名
//                              .antMatchers("/make.html").hasAuthority("normal")
                                //access使用
//                              .antMatchers("/make.html").access("hasAnyRole('admin')")
                                //定义特定IP访问  在authorizeRequests中
                              .antMatchers().hasIpAddress("127.0.0.1")
                             //所有请求都要被认证授权就是登录后才能访问
                            .anyRequest().authenticated();
                              //所有请求都要被认证授权就是登录后才能访问  传入登录请求URI
//                            .anyRequest()
//                            .access("@myAuthentiCationImpl.isAuthenti(request,authentication)");
                })//放行页面的
                .formLogin()
                .usernameParameter("username")     //可以通过这个方法对表单name值对应
                .passwordParameter("password")
                .loginPage("/login.html")
                .loginProcessingUrl("/login")
                //successForwardUrl只能实现请求转发链接,不能实现重定向!
                .successForwardUrl("/demo1")
                //自定义成功跳转方法实现重定向  一般前后端分离的项目就是使用重定向访问数据
                //自定义类与successForwardUrl不能共存
//                .successHandler(new SuccessRedirectHandle("/security/successLogin"))
                //只能实现请求转发
    //          .failureForwardUrl("/errordemo")
                //登录失败页面也是一样 可以自定义实现重定向 与failureForwardUrl不同存在
                .failureHandler(new FailRedirectHandle("error.html"))
                .and()
                .exceptionHandling().accessDeniedHandler(denyAccessExceptionDemo)
                .and()
                //默认保存2周  记住我
                .rememberMe()
                .rememberMeParameter("remember")
                .tokenValiditySeconds(60)   //设置失效时间 60s
                .tokenRepository(getPersistent)
                .userDetailsService(userDetailService)
                .and()
                .logout().logoutUrl("/logout")
                .logoutSuccessUrl("/goLogin")
                .and()
                .csrf().disable();  //关闭CSRF
        return http.build();
    }

}

UserDetails是一个接口,处理用户名和密码的内置接口,要想返回 UserDetails 的实例就只能返回接口的实现类。SpringSecurity 中提供了如下的
实例。对于我们只需要使用里面的 User 类即可。注意 User 的全限定路径是:org.springframework.security.core.userdetails.User 此处经常和系统中自己开发的User 类弄混。

image-20230929225443060

密码处理类

Spring Security 要求容器中必须有 PasswordEncoder 实例。所以当自定义登录逻辑时要
求必须给容器注入 PaswordEncoder 的bean对象。
方法:
encode() :把参数按照特定的解析规则进行解析。
matches() :验证从存储中获取的编码密码与编码后提交的原始密码是否匹配。
如果密码匹配,则返回 true;如果不匹配,则返回 false。第一个参数表示需要被解析的密码。第二个参数表示存储的密码。
upgradeEncoding() :如果解析的密码能够再次进行解析且达到更安全的结果则返回 true,否则返回 false。默认返回 false。

官方推荐BCryptPasswordEncoder实现类,BCryptPasswordEncoder 是对 bcrypt 强散列方法的具体实现。是基于Hash算法实现的单向加密。可以通过strength控制加密强度,默认 10.

配置类

 @Configuration
public  class PasswordEncodeUtilsConf{  

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

测试使用

 @Service
public class UserDetailService implements UserDetailsService {   
    @Resource
    private PasswordEncoder getPwd;
    ......
}

四、配置自定义登录成功页

  • 常用授权配置类中的方法

image-20231001202513246

image-20231001202659394

登录页 (必须为post请求,否则无法跳转,视图解析器不支持post请求,需要引入模板引擎或者使用重定向跳转页面!)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登录页</title>
</head>
<body>
<!--name值必须为username和password-->
   <form action="login" method="post">
       用户名:<input type="text" name="username"><br>
       密码:<input type="password" name="password"><br>
         <input type="checkbox" value="true" name="remember"> 记住我
       <button type="submit">登录</button>
   </form>
</body>
</html>

登录逻辑层

@Service
public class UserDetailService implements UserDetailsService {

    @Resource
    private PasswordEncoder getPwd;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //获取前台的username值
        if(!"admin".equals(username)){
            throw new UsernameNotFoundException("用户名不存在!");
        }
          //配置角色名的时候要加上ROLE_前缀  角色名和资源名严格区分大小写
         //可配置多个用逗号分割。
        List<GrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_admin,normal,ROLE_root,ROLE_sys,/security/successLogin,/security/main.html");
        //构造参数有用户名 密码  权限名(不为空)
        User user = new User(username,getPwd.encode("123456"),authorities);
        return user;
    }
}

controller层

package com.zwf.spring_security.controller;

import org.springframework.security.access.annotation.Secured;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

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

/**
 * @author Mr Zeng
 * @version 1.0
 * @date 2023-09-29 10:38
 */
@Controller
public class LoginController {

     //视图解析器不支持post请求

    //登录成后跳转的路径
    @RequestMapping("/successLogin")
    public String goLoginDo(){
        return "redirect:main.html";  //重定向访问
    }
    //拥有ROLE_admin或者admin角色的才能访问
//    @Secured("admin")
    @PreAuthorize("hasRole('ROLE_admin')")
    @RequestMapping("/goMake")
    public String goMake(){
        return "redirect:make.html";  //重定向访问
    }

    @RequestMapping("/errordemo")
    public String goErrorPage(){
        return "redirect:error.html";  //重定向访问
    }

    @RequestMapping("/exception")
    public void  exception(HttpServletResponse response) throws IOException {
        PrintWriter writer = response.getWriter();
        writer.print("{'code':'403','msg':'权限不足!','data',''}");
        writer.flush();
        writer.close();
    }

    @RequestMapping("/demo1")
    public String goDemo(){
        return "demo";  //使用thymeleaf模板引擎后的访问方法
    }

    @RequestMapping("/goLogin")
    public String goLogin(){
        return "redirect:login.html";
    }

}

自定义登录成功跳转处理配置类

public class SuccessRedirectHandle implements AuthenticationSuccessHandler {
    private  String pageUrl;

    public SuccessRedirectHandle() {
    }

    public SuccessRedirectHandle(String pageUrl) {
        this.pageUrl = pageUrl;
    }

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        //可以在页面重定向跳转时 进行一些授权操作
        //获取登录对象的用户详情对象
        User user = (User)authentication.getPrincipal();
        System.out.println("登录用户名:"+user.getUsername());
        String remoteHost = request.getRemoteHost();
        System.out.println(remoteHost);
        //由于考虑到密码安全  所以密码获取不到
        System.out.println("登录密码:"+user.getPassword());
        response.sendRedirect(pageUrl);
    }
}

登录配置类(在核心配置类中,前提要放行登录页、错误页)

             http.formLogin()
                .usernameParameter("username")     //可以通过这个方法对表单name值对应
                .passwordParameter("password")
                .loginPage("/login.html")
                .loginProcessingUrl("/login")
                //successForwardUrl只能实现请求转发链接,不能实现重定向!
                .successForwardUrl("/demo1")
                //自定义成功跳转方法实现重定向  一般前后端分离的项目就是使用重定向访问数据
                //自定义类与successForwardUrl不能共存
//                .successHandler(new SuccessRedirectHandle("/security/successLogin"))
                //只能实现请求转发
    //          .failureForwardUrl("/errordemo")
                //登录失败页面也是一样 可以自定义实现重定向 与failureForwardUrl不同存在
                .failureHandler(new FailRedirectHandle("error.html"))

访问登录页 不会使用默认登录页 会直接跳转到自定义登录页!

五、自定义成功处理类

由于默认的successForwardUrl只能实现请求转发链接,不能实现重定向!需要自定义实现重定向,实现前后端分离接口跨域访问!

自定义成功处理类

package com.zwf.spring_security.pagehandler;

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

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

/**
 * @author Mr Zeng
 * @version 1.0
 * @date 2023-09-29 16:27
 */

public class SuccessRedirectHandle implements AuthenticationSuccessHandler {
    private  String pageUrl;

    public SuccessRedirectHandle() {
    }

    public SuccessRedirectHandle(String pageUrl) {
        this.pageUrl = pageUrl;
    }

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        //获取登录对象的用户详情对象
        User user = (User)authentication.getPrincipal();
        System.out.println("登录用户名:"+user.getUsername());
        String remoteHost = request.getRemoteHost();
        System.out.println(remoteHost);
        //由于考虑到密码安全  所以密码获取不到
        System.out.println("登录密码:"+user.getPassword());
        response.sendRedirect(pageUrl);
    }
}

在核心配置类中使用(springboot2.7及以上版本使用以下方法)

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    
    http..formLogin().successHandler(new SuccessRedirectHandle("/security/successLogin"))
     ...
    }

六、自定义登录失败页

定义原因跟成功跳转页的局限性一样!

自定义登录失败跳转页

package com.zwf.spring_security.pagehandler;

import org.springframework.security.core.AuthenticationException;
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 Mr Zeng
 * @version 1.0
 * @date 2023-09-29 16:47
 */
//重写失败页面跳转重定向方法
public class FailRedirectHandle implements AuthenticationFailureHandler {

    private String pageUrl;

    public FailRedirectHandle(String pageUrl) {
        this.pageUrl = pageUrl;
    }

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

在核心配置类文件中使用(2.7及以上版本的springboot用以下方法授权)

@Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    
    http.formLogin().failureHandler(new FailRedirectHandle("error.html"))
     ...
    }

七、访问控制URL方法(在springsecurity核心配置类中)

http.formLogin():可以通过里面的内置方法自定义登录页面
http.authorizeRequests():主要对URL进行控制,也就是我们说的授权,支持链式编程,可以通过内置方法对一些请求进行放行(比如静态文件等)。
http.authorizeRequests().anyRequest().authenticated():表示所有请求都要认证,必须放在最后面。

常用的方法

  • antMatches(String pattern,...):可以匹配多个路径,支持ant表达式,也可以匹配具体路径。

image-20231001203527869

  • regexMatchers(String pattern,....):可以匹配多个正则表达式。

内置对象可用作方法参数,对具体请求方法的请求路径进行认证。

  • HttpMethod枚举类

image-20231001204018773

  • MvcMatches()

使用与配置sevelet.contextPath路径情况。

spring.mvc.servlet.path=/xxxx

方法基本使用sevletPath()方法只有mvcMatchers()方法拥有,如果使用其他方法只需使用多层路径/XX/..

.mvcMatchers("/demo").servletPath("/xxxx").permitAll()

内置访问控制方法

  • permitAll()表示所匹配的URL任何人都允许访问

  • authenticated()表示所匹配的URL都需要被认证才能访问。

  • anonymous()表示可以匿名访问匹配的URL,和permitAll()效果类似,只是设置为anonymous()的url会执行filter链中。

  • denyAll()表示匹配的URL都不允许被访问

  • rememberMe():表示勾选记住我才能被访问。

  • fullyAuthenticated()表示没有使用记住我功能才能被访问。

角色权限判断方法,参数值严格区分大小写。

  • hasAuthority(String):表示拥有这个权限的用户才能访问该请求。
.antMatchers("/main.html").hasAuthority("admin");//拥有admin权限的用户可以访问main.html页面
  • hasAnyAuthority(String ...):可以写多个权限名,进行认证
  • hasRole(String):表示拥有此角色的用户才能访问,需要在service中授权角色名,注意需要加ROLE_前缀。
.antMatchers("/main.html").hashasRole("root");
  • hasAnyRole(String...):可以配置多个角色名进行认证!

  • hasIpAddress(String ip):可以配置主机Ip认证,严格区分Ip,localhost和127.0.0.1是不一样的,通过request.getRemoteAddr()获取Ip地址。

八、自定义403异常页面

出现403响应码一般就是权限不足!

//自定义类通过IOC容器实例化然后依赖注入
http.exceptionHandling().accessDeniedHandler(denyAccessExceptionDemo);

自定义异常页面(要实现 AccessDeniedHandler接口)

@Component
public class DenyAccessExceptionDemo implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
         response.setContentType("application/json;charset=utf8");
         response.setStatus(HttpServletResponse.SC_FORBIDDEN);
        PrintWriter writer = response.getWriter();
        writer.write("{'msg':'权限不足!','code':'403'}");
        writer.flush();
        writer.close();
    }
}

九、Access()方法使用

Access()方法中参数可以直接写内置授权认证方法,比如hasRole()、hasAuthentication()方法。

常用的内置方法

image-20231001210827485

基本使用方式

image-20231001210859696

自定义方法使用

  • 自定义方法类()
@Service
public class MyAuthentiCationImpl implements AuthentiCationDemo{
    @Override
    public Boolean isAuthenti(HttpServletRequest request, Authentication authentication) {
        Object obj = authentication.getPrincipal();
            if(obj instanceof UserDetails){
                UserDetails user=(UserDetails) obj;
                //获取登录成功后用户的所有权限
                Collection<? extends GrantedAuthority> authorities = user.getAuthorities();
                return authorities.contains(new SimpleGrantedAuthority(request.getRequestURI()));  //获取请求资源名
            }
           return false;
    }
}
  • 使用自定义方法(通过@Bean id名再获取相应方法实例进行使用)
// /security/successLogin对访问请求路径进行授权通过!
.anyRequest()                          .access("@myAuthentiCationImpl.isAuthenti(request,authentication)");

十、基于注解的访问控制

前提要在springboot启动类上开启注解配置

@SpringBootApplication
//开启注解支持  securedEnabled表示开启@Secure()注解   prePostEnabled表示开启@preMethod和@postMethod  @PreAuthorize()表示方法执行前操作  @PostAuthorize()表示方法执行后操作
@EnableGlobalMethodSecurity(securedEnabled=true,prePostEnabled = true)
public class SpringSecurityApplication {

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

}

一般在Controller层使用

//@Secured参数前缀ROLE_可写可不写,表示拥有ROLE_admin或者admin角色的才能访问此方法
    @Secured("admin")
    @PreAuthorize("hasRole('ROLE_admin')") //@PreAuthorize()和@PostAuthorize()参数是方法名
    @RequestMapping("/goMake")
    public String goMake(){
        return "redirect:make.html";
    }

十一、RememberMe功能实现

一般使用此功能,表示可以免登录,保存用户的登录状态!
  • 编写RememberMe配置类
@Configuration
public class RememberMeDemo {
    @Autowired
    private DataSource dataSource;   //使用数据库连接 所以注入datasource数据源对象

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


    @Bean
    public PersistentTokenRepository getPersistent(){
        JdbcTokenRepositoryImpl repository = new JdbcTokenRepositoryImpl();
        repository.setDataSource(dataSource);
        //自动建表  执行完一次要注释  项目已启动就会在数据库中建表保存登录成功后的用户登录状态信息
        //重复使用程序会报错
      repository.setCreateTableOnStartup(true);
        return repository;
    }

}

image-20231001212909553

核心配置类中配置

             http.rememberMe()
                .rememberMeParameter("remember")  //设置特定的表单name值
                .tokenValiditySeconds(60)   //设置失效时间 60s 默认是2周
                .tokenRepository(getPersistent)  //DI依赖注入持久层类,也就是以上自定义类
                .userDetailsService(userDetailService)  //自定义登录用户逻辑处理类

前端代码 (采用复选框设置记住我页面)

<!--name值必须为username和password  必须是post请求方式-->
   <form action="login" method="post">   
       用户名:<input type="text" name="username"><br>
       密码:<input type="password" name="password"><br>
            <!--默认name值是remember-me-->
         <input type="checkbox" value="true" name="remember"> 记住我
       <button type="submit">登录</button>
   </form>

十二、thymeleaf整合security使用

  • 先导入依赖
 <!--thymeleaf模板引擎-->
        <dependency>
            <groupId>org.thymeleaf.extras</groupId>
            <artifactId>thymeleaf-extras-springsecurity5</artifactId>
        </dependency>
        <!--thymeleaf依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
    </dependencies>
       <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

在html中编写声明

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5"
      xmlns:th="http://www.thymeleaf.org">

在html页面中通过sec:authentication=" "获取UsernamePasswordAuthenticationToken中所有getXXX内容,包含父类的getXXX的内容

  • name:登录用户名
  • principal:登录主体,在自定义登录逻辑中是UserDetails
  • credentials:凭证
  • authorities:权限和角色
  • details:实际上就是webAuthenticationDetails实例,可以获取remoteAddress和sessionId。

快速使用

!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
<head>
    <meta charset="UTF-8">
    <title>thymeleaf页面</title>
</head>
<body>
登录账号:<span sec:authentication="name"></span><br/>
登录账号:<span sec:authentication="principal.username"></span>
<br/>
凭证:<span sec:authentication="credentials"></span><br/>
权限和角色:<span sec:authentication="authorities"></span><br/>
客户端地址:<span sec:authentication="details.remoteAddress">

</span><br/>
sessionId:<span sec:authentication="details.sessionId"></span>
<br/>
<a href="logout">注销</a>
</body>

thymeleaf控制转发必须使用模板引擎视图解析器,跳转到相应页面

image-20231001214852743

用户自定义设置请求路径权限

image-20231001215020184

前端使用效果

image-20231001215122421

十三、登录注销

实际上就是执行/logout请求,然后底层删除cookie和销毁session。

在核心配置类中使用

http.logout().logoutUrl("/logout")  //处理注销请求的路径
                .logoutSuccessUrl("/goLogin");//注销成功跳转到的页面

前端模拟页面

<a href="logout">注销</a>

十四、Csrf跨域请求伪造

CSRF跨站请求伪造,也称为“OneClick Attack”或者session Riding。通过伪造用户请求访问受信任站点的非法请求访问。
跨域:只要网络协议、IP地址,端口号中任何一个不相同就是跨域请求。
由于客服端与服务进行交互时,http本身是无状态协议,所以通过cookie记录客户端身份。在cookie中会存放session Id用来识别客户端身份的,在跨域的情况下,sessionId可能被第三方恶意劫持,通过sessionId向服务器发送请求,服务以为请求是合法的,可能会发生数据泄露等现象。

默认是开启crsf防护,如果我们不关闭就无法请求登录,或者通过模板引擎视图解析器跳转到登录页面的时候获取Crsf令牌。

 <form action="/login" method="post">
        <!--使用Csrf安全防护,获取服务器生成的csrf码 然后请求到服务器 登录成功后就可以访问权限
          注意必须通过请求转发跳转到登录页  才能获取_csrf.token值
      -->
      <input type="hidden" name="_csrf" th:value="${_csrf.token}" th:if="${_csrf}">
        用户名:<input type="text" name="username"><br>
        密码:<input type="password" name="password"><br>
        <input type="submit" value="登录">
    </form>

如果登录页没有设置Csrf 秘钥 就必须在核心配置类中关闭Csrf防护

 http.csrf().disable();  //关闭csrf防护

十五、Oauth2认证方式

简介

Oauth2是一种通用安全接口协议,为用户提供了一个简易开放的标准。同时oauth2兼容多个语言,比如java、php等,使用Oauth2协议,用户不用接触用户名和密码进行授权,比较安全!比如微信在第三方应用认证授权的行为就是属于Oauth2认证授权,用户通过扫码授权,然后到授权服务器获取授权码,然后再用授权码获取令牌,然后使用令牌对资源服务器进行访问,资源服务器对令牌进行校验,校验通过,就响应资源给用户,具体流程如下图:

image-20231002101936549

常用术语

  • 客户凭证:客户端clientId和密码用于认证客户
  • tokens:授权服务器接收客户请求后,颁发的访问令牌。
  • 作用域(scopes):客户请求访问令牌时,由资源拥有者额外的细分权限。
  • 授权码:仅用于授权码授权类型,用于交换获取访问令牌和刷新令牌。
  • 访问令牌:用于代表一个用户或者服务直接去访问受保护的资源。
  • 刷新令牌:用于去授权服务器获取一个刷新访问令牌。
  • bearerToken:不管谁拿到Token都可以访问资源,类似现金。
  • proof of posssession token:校验client是否对Token有明确的拥有权。

授权模式

  • 授权码授权模式 authorization_code(常用)

image-20231002104158004

  • 简化授权模式 implicit

image-20231002104431279

  • 密码授权模式 password

image-20231002104554818

客户端授权模式

image-20231002104617293

刷新令牌授权模式(令牌过期后,通过刷新令牌直接获取新的令牌,简化了操作)

image-20231002104720960

spring security oauth2架构图

image-20231002104935999

客户通过用户代理访问授权服务器,在授权点进行授权,在授权服务器令牌业务逻辑类进行获取授权码,然后通过授权码再访问授权码获取令牌,然后携带令牌访问资源服务器,资源服务器对令牌进行校验,校验成功后获取资源。

十六、springBoot security oauth2 授权码模式

导入依赖(服务端)

<!--导入解析jwt秘钥的依赖-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>
  
     <!--spring-security-oauth2自动装配依赖-->
      <dependency>
            <groupId>org.springframework.security.oauth.boot</groupId>
            <artifactId>spring-security-oauth2-autoconfigure</artifactId>
            <version>2.3.5.RELEASE</version>
        </dependency>
     <!--spring-security依赖-->
        <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>

编写用户自定义详情类

public class User implements UserDetails {
    private String username;
    private String password;
    private Collection<? extends GrantedAuthority> authorities;

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

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return this.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;
    }
}

用户权限业务处理类

@Service
public class LoginService implements UserDetailsService {

    @Resource
    private PasswordEncoder getBcryptPwd;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        if(username==null||"".equals(username)){
            throw new UsernameNotFoundException("用户名不存在!");
        }
        //这里对登录后进行授权  授权角色要加ROLE_前缀
        return new User("root",getBcryptPwd.encode("root"), AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_admin,ROLE_root,normal"));
    }
}

Controller类资源请求路径

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

     @RequestMapping("/getCurrentUser")
    public Object getCurrentUser(Authentication authentication){
        return authentication;
    }

}

编写权限配置类

@Configuration
@EnableWebSecurity  //开启授权配置
public class SecurityConfig extends WebSecurityConfigurerAdapter  {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeRequests()
                .antMatchers("/oauth/**","/login/**","logout/**")
                .permitAll()
                .anyRequest()
                .authenticated()
                .and()
                .formLogin()
                .permitAll();
    }
      //密码加密类
    @Bean
    public PasswordEncoder getBcryptPwd(){

        return new BCryptPasswordEncoder();
    }
     //用于密码授权获取令牌
      @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

配置授权服务器配置类(服务端)

@Configuration
@EnableAuthorizationServer//开启认证服务器
public class AuthentiServerConfig extends AuthorizationServerConfigurerAdapter {

     @Resource
    private PasswordEncoder getBcryptPwd;

     //注入认证管理
    @Autowired
    private AuthenticationManager authenticationManager;

    //注入用户自定义业务处理类
    @Autowired
    private LoginService userDetailsService;

    //注入令牌保存器
//    @Resource
//    private TokenStore getRedisStoreDemo;

    //jwt秘钥
    @Autowired
    private TokenStore jwtTokenStore;

    //jwt转换器依赖注入
    @Autowired
    private JwtAccessTokenConverter jwtAccessTokenConverter;

    //TokenHancer 依赖注入
    @Autowired
    private  TokenHancerConfig tokenHancerConfig;


    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        //配置客户端在内存中运行
        clients.inMemory()
                //配置clientId
                .withClient("admin")
               //配置密码
                .secret(getBcryptPwd.encode("admin"))
                //配置token有效期  3600s
                .accessTokenValiditySeconds(3600)
                //设置属性刷新token有效期  3600s
                //下次直接通过 grant_type=refresh refresh_token="" 请求获得有效token
                .refreshTokenValiditySeconds(3600)
                .autoApprove(true)
                //配置授权成功后跳转到客户端登录页面进行授权
                //.redirectUris("http://localhost:9998/login")
                //模拟测试跳转到百度页面获取授权码  授权会在跳转链接后展示在浏览器路径框
                .redirectUris("http://www.baidu.com")
                //配置配置申请的范围
                .scopes("all")

                //配置授权类型 (授权码授权)
//                .authorizedGrantTypes("authorization_code");
                 //授权方式 可以配置多个 "refresh_token","authorization_code"
                .authorizedGrantTypes("authorization_code","refresh_token");
    }

      //配置密码访问授权所需要的配置
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        //创建TokenEnhancerChain
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
         //把 自定义enhancer 和  转换器放入
        List<TokenEnhancer> list=new ArrayList<>();
        list.add(tokenHancerConfig);
        list.add(jwtAccessTokenConverter);
        tokenEnhancerChain.setTokenEnhancers(list);
        endpoints.authenticationManager(authenticationManager)
                .userDetailsService(userDetailsService)
//                .tokenStore(getRedisStoreDemo);//把oauth2 token存入redis中
                .tokenStore(jwtTokenStore)
                .accessTokenConverter(jwtAccessTokenConverter)
                .tokenEnhancer(tokenEnhancerChain);
    }

    //使用单点登录必须配置

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

配置资源服务器

//开启资源服务配置
@Configuration
@EnableResourceServer  //开启资源服务配置
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

      //配置需要保护的资源
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and() //注意and()隔开
                .requestMatchers()
                .antMatchers("/user/**");  //配置需要保护的访问资源请求路径
    }
}

用链接进行测试

http://localhost:8080/oauth/authorize?response_type=code&client_id=admin&redirect_u ri=http://www.baidu.com&scope=all

跳转到登录先登录

image-20231002111639281

登录成功后进行授权操作

image-20231002111745371

跳转到百度页面 获取授权码

image-20231002111608435

根据授权码在postMan进行测试 (获取令牌必须用post请求方式)

image-20231002112553989

请求参数如下

image-20231002113105393

image-20231002113209896

携带令牌参数访问资源路径获取资源

image-20231002113509835

令牌失效

image-20231002114145596

  • 密码授权模式

在以上架构基础上 授权服务配置类中配置加上以下

 //注入用户自定义业务处理类
    @Autowired
    private LoginService userDetailsService;


//注入认证管理
    @Autowired
    private AuthenticationManager authenticationManager;


//在授权服务器配置方法中配置以下
//授权方式 可以配置多个 "refresh_token","authorization_code" 配置密码授权模式
.authorizedGrantTypes("password","refresh_token");



//配置密码访问授权所需要的配置
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.authenticationManager(authenticationManager)
                .userDetailsService(userDetailsService);
    }

PostMan测试通过密码模式获取令牌 必须用post请求

image-20231002115228992

image-20231002115420084

通过令牌获取资源

image-20231002115556870

十七、令牌保存在redis中

  • 导入依赖
       <dependency>
           <groupId>org.springframework.boot</groupId>-->
           <artifactId>spring-boot-starter-data-redis</artifactId>-->
       </dependency>
  • 配置redis连接工厂类
@Configuration
public class RedisStoreConfig {
    @Autowired
   private RedisConnectionFactory redisConnectionFactory;

      //TokenStore  把oauth2 token存入redis中
      @Bean
     public TokenStore getRedisStoreDemo(){

         return new RedisTokenStore(redisConnectionFactory);
   }

}
  • 授权服务器配置类

  //注入用户自定义业务处理类
    @Autowired
    private LoginService userDetailsService;

    //注入令牌保存器  redis配置类中的Bean
    @Resource
    private TokenStore getRedisStoreDemo;

       //注入认证管理
    @Autowired
    private AuthenticationManager authenticationManager;



  //配置密码访问授权所需要的配置
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.authenticationManager(authenticationManager)
                .userDetailsService(userDetailsService)
               .tokenStore(getRedisStoreDemo); //把oauth2 token存入redis中
         
    }
  • 在yml配置redis连接信息
server:
  port: 9999

spring:
  redis:
    host: 192.168.147.110
    password: root@123456

postMan测试密码模式获取令牌

image-20231002120730620

令牌保存在redis中

image-20231002120838007

十八、常见的认证模式

http basic Auth:传统的用户名、密码登录

cookie Auth:把用户信息放进cookie中,cookie中的信息与服务器中session比对

Oauth:访问第三方应用时,无需用户名密码,通过签发的令牌验证登录。

Token Auth:用户通过用户名和密码登录成功后,会签发一个token令牌,token令牌保存在cookie中,每次访问资源路径时就携带令牌数据,资源服务器对令牌进行校验,校验成功后获取资源。

Token可以跨域访问、Cookie不能跨域访问,Token因为不用依靠Cookie,因此不需要Csrf。

十九、JWT简介

简介

JSON Web Token(JWT)是一个开放的行业标准(RFC 7519),它定义了一种简介的、自包含的协议格式,用于在通信双方传递json对象,传递的信息经过数字签名可以被验证和信任。JWT可以使用HMAC算法或使用RSA的公钥/私钥对来签名,防止被篡改。

JWT组成成分

请求头、载荷、签名。
三部分都是由Base64加密,签名是由请求+载荷+盐值经过Base64加密算法加密而来,因此盐值要保密,不要泄露,否则很容易被破解签名。

三部分通过.连接Base64加密后形成JWT

image-20231002123441941

请求头

image-20231002123520159

负载(载荷)

image-20231002123558203

image-20231002123649265

签证

image-20231002123802669

二十、JWT快速入门

导入依赖

 <!--用于解析jwt秘钥-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>

        <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>
  • 测试JWT常用方法
@SpringBootTest
class JwtDemoApplicationTests {

   //测试创建Token
    @Test
    public void testCreateToken(){
        JwtBuilder builder = Jwts.builder();
         //声明的标识 => {jti:8888}
        builder.setId("8888")
                //用户主体 {sub:Rose} 公钥面向用户
                .setSubject("Rose")
                //创建时间  {ita:XXX}
                .setIssuedAt(new Date())
                //设置签名手段壹以及盐值  必须使用HS开头的加密方式
                .signWith(SignatureAlgorithm.HS256,"ssssss");
        //获取Token
        String token = builder.compact();
        System.out.println(token);

        //使用BASE64解密
        String[] split = token.split("\\.");
        System.out.println("加密方式:"+Base64Codec.BASE64.decodeToString(split[0]));
        System.out.println("[标识,用户主体,时间]:"+Base64Codec.BASE64.decodeToString(split[1]));
        System.out.println("签名内容:(无法显示)"+Base64Codec.BASE64.decodeToString(split[2]));

    }

    //jwt Token解密解析声明的信息
     @Test
    void decodeJwtToken(){
        String taken="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODg4Iiwic3ViIjoiUm9zZSIsImlhdCI6MTY5NjA4MTUxNX0.uWcsx0HJSpc87usI2AiVfulUvPw4AwEiSbUqQxuMt5A";
         Claims cls = Jwts.parser()
                 //设置盐值
                 .setSigningKey("ssssss")
                 //设置token
                 .parseClaimsJws(taken)
                 //获取声明内容
                 .getBody();
         //打印声明的属性
         System.out.println("jwt标识:"+cls.getId());
         System.out.println("jwt主体:"+cls.getSubject());
         System.out.println("jwt时间:"+cls.getIssuedAt());
     }

      //设置过期时间  增加声明
      @Test
    public void expireJwt(){
        Long currentTime=System.currentTimeMillis();
        Long expireDate=currentTime+60*1000;  //当前时间后的一分钟就是过期时间  单位毫秒
          String token = Jwts.builder().setId("8888")
                  .setSubject("Rose")
                  .setIssuedAt(new Date())
                  .signWith(SignatureAlgorithm.HS256, "ABCD")
                  .setExpiration(new Date(expireDate))//设置过期时间
                  //增加声明
                  .claim("user","admin")
                  .claim("pwd","yyyyyy")
                  .compact();
          System.out.println(token);
      }
     //获取token声明的信息
      @Test
      void  parseJwt(){
        String token="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODg4Iiwic3ViIjoiUm9zZSIsImlhdCI6MTY5NjA4MjgzNCwiZXhwIjoxNjk2MDgyODk0LCJ1c2VyIjoiYWRtaW4iLCJwd2QiOiJ5eXl5eXkifQ.WbxVv32zbBMcRQSALqoIQX5dSUEDUSjXJBxYz6f5j1U";
         Claims body = Jwts.parser()
                 .setSigningKey("ABCD")
                 .parseClaimsJws(token)
                 .getBody();
          SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
          System.out.println("[声明的标识]"+body.getId());
          System.out.println("[声明的主体]"+body.getSubject());
          System.out.println("[声明的时间]"+sdf.format(body.getIssuedAt()));
          System.out.println("[当前时间]"+sdf.format(new Date()));
          System.out.println("[设置的过期时间]"+sdf.format(body.getExpiration()));
          System.out.println("[获取的增加的声明]"+body.get("user"));
          System.out.println("[获取的增加的声明]"+body.get("pwd"));

      }

}

二十一、springsecurity Oauth2整合Jwt

导入依赖

    <!--使用oauth2-->
        <dependency>
            <groupId>org.springframework.security.oauth</groupId>
            <artifactId>spring-security-oauth2</artifactId>
            <version>2.3.5.RELEASE</version>
        </dependency>
         <!--spring-oauth2自动装配依赖-->
        <dependency>
            <groupId>org.springframework.security.oauth.boot</groupId>
            <artifactId>spring-security-oauth2-autoconfigure</artifactId>
            <version>2.3.5.RELEASE</version>
        </dependency>
        <!--springSecurity-->
        <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>
        <!--用于解析jwt声明信息-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </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>
       

配置JWT核心配置类

//配置JWt配置类
@Configuration
public class JwtConfig {

    //token转换器  用于jwt令牌转换为Oauth2令牌
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter(){
        JwtAccessTokenConverter jatc=new JwtAccessTokenConverter();
            //配置jwt使用的秘钥key值
        jatc.setSigningKey("zwf");
           return jatc;   //
    }

   //设置TokenStore保存器
      @Bean
    public TokenStore jwtTokenStore(){
        return new JwtTokenStore(jwtAccessTokenConverter());
    }
    
    //把自定义的token增强类 注入Bean中
    @Bean
    public TokenHancerConfig tokenHancerConfig(){
        return new TokenHancerConfig();
    }


}

在授权服务器配置类中配置

@Configuration
@EnableAuthorizationServer//开启认证服务器
public class AuthentiServerConfig extends AuthorizationServerConfigurerAdapter {
    //配置密码加密方式
     @Resource
    private PasswordEncoder getBcryptPwd;

     //注入认证管理
    @Autowired
    private AuthenticationManager authenticationManager;

    //注入用户自定义业务处理类
    @Autowired
    private LoginService userDetailsService;

 
    //jwt秘钥  自定义jwt配置类
    @Autowired
    private TokenStore jwtTokenStore;

    //jwt转换器依赖注入
    @Autowired
    private JwtAccessTokenConverter jwtAccessTokenConverter;


    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        //配置客户端在内存中运行
        clients.inMemory()
                //配置clientId
                .withClient("admin")
               //配置密码
                .secret(getBcryptPwd.encode("admin"))
                //配置token有效期  3600s
                .accessTokenValiditySeconds(3600)
                //设置属性刷新token有效期  3600s
                //下次直接通过 grant_type=refresh refresh_token="" 请求获得有效token
                .refreshTokenValiditySeconds(3600)
                .autoApprove(true)
                //配置授权成功后跳转到客户端页面
                .redirectUris("http://localhost:9998/login")
                //配置配置申请的范围
                .scopes("all")

                //配置授权类型 (授权码授权)
//                .authorizedGrantTypes("authorization_code");
                 //密码授权 "refresh_token","authorization_code"
                .authorizedGrantTypes("authorization_code");
    }

      //配置密码访问授权所需要的配置
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
       
        endpoints.authenticationManager(authenticationManager)
                .userDetailsService(userDetailsService)
                .tokenStore(jwtTokenStore)  //jwt令牌保存策略
                .accessTokenConverter(jwtAccessTokenConverter); //配置jwt转换器转换器
                
    }

}

PostMan使用密码授权模式获取Jwt

image-20231002130006824

官网解析

image-20231002130410859

二十二、Jwt内容增强器

自定义Token增强类

/秘钥增强配置

public class TokenHancerConfig implements TokenEnhancer {

        @Override
        public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
            //Token增加的类容 用于加强Token秘钥复杂度
            Map<String,Object> info = new HashMap<>();
            info.put("enhance","enhance info");
            //把参数强转
            ((DefaultOAuth2AccessToken)accessToken).setAdditionalInformation(info);
            return accessToken;
        }
    }

在jwt配置类中配置TokenEnhancer IOC实例对象


 //把以上自定义的token增强类 注入Bean中 IOC容器托管
    @Bean
    public TokenHancerConfig tokenHancerConfig(){

        return new TokenHancerConfig();
    }

在授权服务器配置类中配置

    //jwt转换器依赖注入
    @Autowired
    private JwtAccessTokenConverter jwtAccessTokenConverter;

  //TokenHancer 依赖注入
    @Autowired
    private  TokenHancerConfig tokenHancerConfig;


//配置密码访问授权所需要的配置
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        //创建TokenEnhancerChain
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
         //把 自定义enhancer 和  jwtAccessTokenconverter转换器放入集合中
        List<TokenEnhancer> list=new ArrayList<>();
        list.add(tokenHancerConfig);  //集合存放 自定义TokenHancer
        list.add(jwtAccessTokenConverter); //集合保存 jwt转换器
        //把集合内容放入 TokenEnhancerChain类中
        tokenEnhancerChain.setTokenEnhancers(list);
        
        endpoints.authenticationManager(authenticationManager)
                .userDetailsService(userDetailsService)
                .tokenStore(jwtTokenStore)  //jwt token保存策略
                .accessTokenConverter(jwtAccessTokenConverter)
                //配置TokenEnhancerChain
                .tokenEnhancer(tokenEnhancerChain);
    }

image-20231002132251654

二十三、解析Jwt声明信息

  • 导入依赖
   <!--用于解析jwt声明信息-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>
  • 自定义Controller类请求解析
@RestController
@RequestMapping("/user")
public class UserController {

     @RequestMapping("/getCurrentUser")
    public Object getPrincipal(Authentication authentication, HttpServletRequest request){
         //解析token中的声明信息
         String header = request.getHeader("Authorization");
         //获取Token
         String token = header.substring(header.indexOf("bearer") + 7);
         System.out.println(token);
          //解析jwt声明的信息  返回json字串
         return Jwts.parser().setSigningKey("zwf".getBytes(StandardCharsets.UTF_8))
                     .parseClaimsJws(token)
                     .getBody();
    }
}
  • 使用Postman模拟请求

image-20231002132651208

二十四、Oauth2+Jwt模拟单点登录(SSO)

模拟服务端

  • 导入依赖
 <!--使用oauth2-->
        <dependency>
            <groupId>org.springframework.security.oauth</groupId>
            <artifactId>spring-security-oauth2</artifactId>
            <version>2.3.5.RELEASE</version>
        </dependency>
        <!--springSecurity-->
        <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.thymeleaf.extras</groupId>
            <artifactId>thymeleaf-extras-springsecurity5</artifactId>
        </dependency>-->
        <!--用于解析jwt声明信息-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </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>

        <dependency>
            <groupId>org.springframework.security.oauth.boot</groupId>
            <artifactId>spring-security-oauth2-autoconfigure</artifactId>
            <version>2.3.5.RELEASE</version>
        </dependency>

  • 配置好相关配置类,具体见上文已经阐述

image-20231002133144085

唯一注意的是,授权服务器配置类设置授权成功重定页面要设置为客户端请求路径

 @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        //配置客户端在内存中运行
        clients.inMemory()
                //配置clientId
                .withClient("admin")
               //配置密码
                .secret(getBcryptPwd.encode("admin"))
                //配置token有效期  3600s
                .accessTokenValiditySeconds(3600)
                //设置属性刷新token有效期  3600s
                //下次直接通过 grant_type=refresh refresh_token="" 请求获得有效token
                .refreshTokenValiditySeconds(3600)
                .autoApprove(true)  //自动授权也可以不设置进行手动授权
                //配置授权成功后跳转到客户端页面  (特别注意!!!!1)
                .redirectUris("http://localhost:9998/login")
                //配置配置申请的范围
                .scopes("all")
                //配置授权类型 (授权码授权)
//                .authorizedGrantTypes("authorization_code");
                 //密码授权 "refresh_token","authorization_code"
                .authorizedGrantTypes("authorization_code","password");
    }
  • 配置好自定义用户业务逻辑类,具体见上文!

image-20231002133308809

模拟客户端

  • 导入依赖
<!--使用oauth2-->
        <dependency>
            <groupId>org.springframework.security.oauth</groupId>
            <artifactId>spring-security-oauth2</artifactId>
            <version>2.3.5.RELEASE</version>
        </dependency>
        <!--springSecurity-->
        <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.thymeleaf.extras</groupId>
            <artifactId>thymeleaf-extras-springsecurity5</artifactId>
        </dependency>-->
        <!--用于解析jwt声明信息-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </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>

        <dependency>
            <groupId>org.springframework.security.oauth.boot</groupId>
            <artifactId>spring-security-oauth2-autoconfigure</artifactId>
            <version>2.3.5.RELEASE</version>
        </dependency>
  • 配置访问资源Controller
/**
 * @author Mr Zeng
 * @version 1.0
 * @date 2023-10-01 11:22
 */
@RestController
@RequestMapping("/user")
public class UserController {

     @RequestMapping("/getCurrentUser")
    public Object getCurrentUser(Authentication authentication){
        return authentication;
    }

}
  • yml核心配置文件,一定要注意配置文件的key value不要写错了!
server:
  port: 9998   #注意不要与服务端的端口产生冲突   服务端端口和客户端口不能一致
  servlet:
    session:
      cookie:
        name: oauth2-client  #cookie冲突 导致登录验证不通过  多个客户端的必须配置
#全部都是从服务端获取的数据
security:
  oauth2:
    client:
      client-id: admin  #在授权服务器配置类中设置的ClientId
      client-secret: admin   #在授权服务器配置类中设置的密码
      user-authorization-uri: http://localhost:9999/oauth/authorize #授权URI
      access-token-uri: http://localhost:9999/oauth/token   #获取令牌URI
    resource:
      jwt:
        key-uri: http://localhost:9999/oauth/token_key  #获取TOken_key

测试访问 请求客户端资源路径名 :http://localhost:9998/user/getCurrentUser

首先重定向跳转到服务端登录页面,使用用户自定义配置类中设置的用户名和密码进行登录,登录成功后,自动重定向跳转到http://localhost:9998/user/getCurrentUser,整个过程使用了授权码授权方式访问资源目录。

image-20231002134539799

image-20231002134616536

posted @ 2023-10-02 13:51  戴莫先生Study平台  阅读(128)  评论(0编辑  收藏  举报