第11章 Spring Security安全管理框架(四次课)

飞一样的编程
飞一样的编程
擅长邻域:Java,MySQL,Linux,nginx,springboot,mongodb,微信小程序,vue

分类: springboot 专栏: springboot3.0新教材 标签: 安全框架

2024-01-22 11:33:58 2314浏览

springboot3权限框架springsecurity6学习

前期回顾

  1. springcache的作用是啥,概念
  2. 应用场景分析下
  3. 常用的注解

简单介绍

Spring Security 是一个功能强大且高度可定制的身份验证和访问控制框架,提供了完善的认证机制和方法级的授权功能。

Spring Boot 3.0 默认使用的是 Spring Security6.0 版本。配置类与旧版有很大不同,旧版的配置类继承自 WebSecurityConfigurerAdapter 类,并重 写 了 里 面 的 多 个 方 法 , 而 Spring Security6.0 版 本 的 配 置 类 没 有 继 承WebSecurityConfigurerAdapter 类,用户可以直接在配置类里面创建各种 Bean。

本章讲解了Spring Security 的认证、授权、权限控制、JWT 原理等内容,并使用 JWT 进行了前后端分离认证实战,也讲解了 OAuth2 基础知识和 OAuth2 第三方 gitee 登录实战

Spring Security的认证功能

认证即确认用户访问当前系统的身份,只有经过认证的用户才能访问系统特定资源

素材和案例代码准备:

小师弟可以练习普通武功

二师兄可以练习高级武功和普通武功

大师兄所有武功都能练习

这时没用到 security 认证功能,所有页面都能任意访问。下面将添加 Spring Security 的认证功能,只有认证过的用户才能访问

1. 加依赖

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

如果非要纠结这个样式出不来的话,可以翻个墙,其实没必要纠结,因为后面我们压根不用这个默认页面,我们会自定义一个属于自己的登录页面

这个认证默认用户名是 user,密码在项目启动时在控制台给出,

2. 自定义用户名与密码

spring.security.user.name=admin
spring.security.user.password=123456

以上方法只能定义一个用户名与密码,如果需要定义多个用户名与密码就可以通过创建一个配置类,在配置类的UserDetailsService 方法中使用内存方式创建多个用户,设置每个用户的用户名与密码及角色。

@Configuration
public class SecurityConfig {
    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();//加盐加密   md5  e10xxxxx
        //如果不想加密就返回
//        return NoOpPasswordEncoder.getInstance();
    }

     @Bean
    UserDetailsService userDetailsService(){
        InMemoryUserDetailsManager  manager=new InMemoryUserDetailsManager();
        UserDetails user1= User.withUsername("user1").password("$2a$10$xN/Nt37Jy3uvs0PqNdwHoOlyGqikkHfuTcFmVeOtEHoZYPX/mVJiS").roles("role1").build();
        UserDetails user2= User.withUsername("user2").password("$2a$10$xN/Nt37Jy3uvs0PqNdwHoOlyGqikkHfuTcFmVeOtEHoZYPX/mVJiS").roles("role2","role1").build();
        UserDetails user3= User.withUsername("user3").password("$2a$10$xN/Nt37Jy3uvs0PqNdwHoOlyGqikkHfuTcFmVeOtEHoZYPX/mVJiS").roles("role1","role2","role3").build();
        manager.createUser(user1);
        manager.createUser(user2);
        manager.createUser(user3);

        return manager;
    }
}

其中 passwordEncoder()方法表示密码加密方式, 一般使用 BCryptPasswordEncoder 方式。这样就创建了四个用户,创建用户时还需指定其角色,这里暂时这样设置用户角色:

小师弟 二师兄,大师兄 三个角色分别是role1 role2 role3

为三个角色创建三个账号:user1 user2 user3

3. 访问控制

希望实现首页无须登录就能访问,静态资源可以自由访问,其他页面需要登录才能访问,关键步骤如下。

1.在 WebSecurityConfig 类中添加以下方法,允许静态资源直接放行。//白名单 黑名单

 //静态资源直接放行
    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        //忽略这些静态资源(不拦截)  新版本 Spring Security 6.0 已经弃用 antMatchers()
        return (web) -> web.ignoring().requestMatchers("/js/**", "/css/**","/images/**");
    }

2.在 WebSecurityConfig 类中添加 filterChain 方法,开启登录配置,代码如下:

 @Bean
    SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
        httpSecurity.authorizeRequests() //开启登录配置
                .requestMatchers("/").permitAll() //允许直接访问的路径,包括验证码
                .anyRequest().authenticated();//其他任何请求都必须经过身份验证

        httpSecurity.formLogin();//开启表单验证
        return httpSecurity.build();
    }

4. 自定义登录界面与注销登录

上述登录界面是由系统给定的,很多时候登录界面是开发者自定义的

<h1>用户登录</h1>
<form th:action="@{/login}" method="post">
    用户名:<input type="text" name="username"><br/>
    密&nbsp;&nbsp;&nbsp;码:<input type="password" name="password"><br/>
 		<input type="submit" value="登录"><br/>

</form>
 httpSecurity.formLogin()//开启表单验证
                .loginPage("/toLogin")//跳转到自定义的登录页面
                .usernameParameter("username")//自定义表单的用户名的name,默认为username
                .passwordParameter("password")//自定义表单的密码的name,默认为password
                .loginProcessingUrl("/login")//表单请求的地址,使用Security定义好的/login,并且与自定义表单的action一致
                .permitAll();//允许访问登录有关的路径

注销功能实现

 //开启注销
        httpSecurity.logout().logoutSuccessUrl("/");//注销后跳转到index页面
        httpSecurity.csrf().disable();//关闭csrf

访问/logout,即可注销登录,程序回到首页中来。注意/logout 是 Spring Security 默认的退出登录的 URL。

 @Bean
    SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
        httpSecurity.authorizeRequests() //开启登录配置
                .requestMatchers("/","/index").permitAll() //允许直接访问的路径,包括验证码
                .anyRequest().authenticated();//其他任何请求都必须经过身份验证

        httpSecurity.formLogin()//开启表单验证
                .loginPage("/toLogin")//跳转到自定义的登录页面
                .usernameParameter("username")//自定义表单的用户名的name,默认为username
                .passwordParameter("password")//自定义表单的密码的name,默认为password
                .loginProcessingUrl("/login")//表单请求的地址,使用Security定义好的/login,并且与自定义表单的action一致
                .permitAll();//允许访问登录有关的路径
        //开启注销
        httpSecurity.logout().logoutSuccessUrl("/index");//注销后跳转到index页面
        httpSecurity.csrf().disable();//关闭csrf
        return httpSecurity.build();
    }

登录成功后,想直接跳到主页,在formLogin后加

 .defaultSuccessUrl("/",true)
               

5. 登录认证失败的处理

1.在 WebSecurityConfig 的配置类的 filterChain 方法中配置登录失败时跳转到的 URL

 .failureUrl("/loginError")//如果登录失败跳转到这里。记得放行白名单           

2. 错误提示controller


  @GetMapping("/loginError")
    public String error(HttpServletRequest request, Model model){
    //security框架里存的 
      AuthenticationException authenticationException = (AuthenticationException) request.getSession()
                .getAttribute("SPRING_SECURITY_LAST_EXCEPTION");
        if (authenticationException instanceof UsernameNotFoundException || authenticationException instanceof BadCredentialsException) {
            model.addAttribute("msg","用户名或密码错误");
        } else if (authenticationException instanceof DisabledException) {
            model.addAttribute("msg","用户已被禁用");
        } else if (authenticationException instanceof LockedException) {
            model.addAttribute("msg","账户被锁定");
        } else if (authenticationException instanceof AccountExpiredException) {
            model.addAttribute("msg","账户过期");
        } else if (authenticationException instanceof CredentialsExpiredException) {
            model.addAttribute("msg","证书过期");
        }
        return "pages/login";
    }

3.修改前端 login.html 页面,添加显示错误信息的代码

<span th:text="${msg}" style="color:red"></span><br/>

6. 记住用户名

(1) 首先,在 WebSecurityConfig 配置类的 filterChain 方法下添加以下

 httpSecurity.rememberMe(); //记住我

(2) 修改 login.html,添加记住我的选择框,注意该表单项的 name 属性必须是remember-me,关键代码如下。

 记住我:<input type="checkbox" name="remember-me"/><br/>

(3) 测试

勾选“记住我”选择框,登录后进入子菜单项。关闭浏览器,重新打开访问首页,再访问子菜单项,无须登录,直接就能访问,证明系统记住了用户名与密码。

7. 图形验证码

为项目添加验证码功能。在 Spring Security 中需要添加过滤器来实现验证码功能

(1) 创建一个工具类 ValidCode,用于生成验证码。

public class ValidCode {

    private int width = 100;//验证码图片的宽度
    private int height = 40;//验证码图片的高度
    private String[] fontNames = { "宋体", "楷体", "隶书", "微软雅黑" };//可选择的字体
    private Color bgColor = new Color(255, 255, 255);//设置验证码图片的背景颜色为白色
    private Random random = new Random();
    private String codes = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
    private String validcode;// 保存随机数字(即验证码)


    private Color getColor() {  //随机生成一个颜色
        int red = random.nextInt(200);
        int green = random.nextInt(200);
        int blue = random.nextInt(200);
        return new Color(red, green, blue);
    }

    private Font getFont() {//随机字体
        String name = fontNames[random.nextInt(fontNames.length)];
        int style = random.nextInt(4);
        int size = random.nextInt(5) + 24;
        return new Font(name, style, size);
    }


    private char getChar() { //随机字符
        return codes.charAt(random.nextInt(codes.length()));
    }


    private BufferedImage createImage() { //创建BufferedImage对象
        BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        Graphics2D g2 = (Graphics2D) image.getGraphics();
        g2.setColor(bgColor);// 设置验证码图片的背景颜色
        g2.fillRect(0, 0, width, height);
        return image;
    }

    public BufferedImage getImage() {
        BufferedImage image = createImage();
        Graphics2D g2 = (Graphics2D) image.getGraphics();
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < 4; i++) {
            String s = getChar() + "";
            sb.append(s);
            g2.setColor(getColor());
            g2.setFont(getFont());
            float x = i * width * 1.0f / 4;
            g2.drawString(s, x, height - 15);
        }
        this.validcode = sb.toString();
        drawLine(image);
        return image;
    }

    private void drawLine(BufferedImage image) {//绘制干扰线
        Graphics2D g2 = (Graphics2D) image.getGraphics();
        int num = 6;
        for (int i = 0; i < num; i++) {
            int x1 = random.nextInt(width);
            int y1 = random.nextInt(height);
            int x2 = random.nextInt(width);
            int y2 = random.nextInt(height);
            g2.setColor(getColor());
            g2.setStroke(new BasicStroke(1.5f));
            g2.drawLine(x1, y1, x2, y2);
        }
    }

    public String getValidcode() {
        return validcode;
    }

    //输出JPEG图片到前端
    public static void output(BufferedImage image, OutputStream out) throws IOException {
        ImageIO.write(image, "JPEG", out);
    }
}

(2) 创建过滤器 ValidCodeFilter,在代码中设置为只过滤登录程序/login。


@Component
public class ValidCodeFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        //下面代码表示只过滤/login
        if ("POST".equalsIgnoreCase(request.getMethod()) && "/login".equals(request.getServletPath())) {
            String requestCode = request.getParameter("code"); //从前端获取用户填写的验证码
            String validcode = (String) request.getSession().getAttribute("validcode");
            if (!validcode.toLowerCase().equals(requestCode.toLowerCase())) { // 验证码不相同就跳转
                //手动设置异常
                request.getSession().setAttribute("msg","验证码错误");//存储错误信息,以便前端展示
                response.sendRedirect("/toLogin");
                 return;//不让走后面的放行代码
            }
        }
        filterChain.doFilter(request, response);//验证码相同放行
    }


}

(3)修改配置类 WebSecurityConfig,注入过滤器如下

    @Autowired
    ValidCodeFilter validCodeFilter;

再修改 filterChain 方法,在第一行添加下述代码

  httpSecurity.addFilterBefore(validCodeFilter, UsernamePasswordAuthenticationFilter.class);

意思是在认证用户名与密码之前加一个 validCodeFilter 过滤器,这样将先验证验证码,验证码通过下一步才验证用户名与密码。

(4) 在控制器 SecurityController 中添加如下代码,功能是访问/validcode 即可输出验证码图片。

  @GetMapping("/validcode")
    public void getValidPicture(HttpServletRequest request, HttpServletResponse response) throws IOException {
        ValidCode validCode = new ValidCode();
        BufferedImage image = validCode.getImage();
        String validcode = validCode.getValidcode();//获取随机验证码(字符串)
        System.out.println("validcode:"+validcode);
        HttpSession session = request.getSession();
        session.setAttribute("validcode", validcode);//将随机验证码存入session
        validCode.output(image, response.getOutputStream());//输出图片
    }

(5)放行访问路径/validcode。修改filterChain 方法直接放行的有关代码如下:

.requestMatchers("/","/index","/validcode").permitAll() //允许直接访问的路径,包括验证码
                

(6)修改 login.html,添加如下代码

验证码:<input type="text" name="code"><img src="/validcode"  id="codeImg" width="100" height="40"/><br/>

补充,点击图片后换验证码图片,需要引入jQuery

 <dependency>
            <groupId>org.webjars</groupId>
            <artifactId>jquery</artifactId>
            <version>3.6.0</version>
        </dependency>

<script src="/webjars/jquery/3.6.0/jquery.min.js"></script>
 function changeCode() {
        $("#codeImg").attr("src","/validcode")
    }

(7)获取验证码错误信息。需要在控制器的 toLogin 方法中获取有关的 Session,修改 toLogin 方法如下:

 @GetMapping("/toLogin") //自定义登录页的访问路径
    public String toLogin(HttpServletRequest request){
        //回显
        String msg= (String) request.getSession().getAttribute("msg");
        if(msg!=null){
            request.setAttribute("msg",msg);
        }

      return "login";
    }

(8) 运行测试。

Spring Security的授权功能

1. 自定义用户授权

用户授权简单来说就是规定什么用户可以访问什么页面,不同的用户可以访问不同的页面。简单的可以在配置类中直接指定,复杂的需要整合数据库。这里先学习简单的自定义授权。

修改 filterChain 方法中的 httpSecurity.authorizeRequests() 相关配置如下。

        httpSecurity.authorizeRequests() //开启登录配置
                .requestMatchers("/","/index","/validcode").permitAll() //允许直接访问的路径,包括验证码
                .requestMatchers("/level1/**").hasRole("role1")//用户需要有role1的角色才能访问/menu1/**
                .requestMatchers("/level2/**").hasRole("role2")
                .requestMatchers("/level3/**").hasRole("role3")
                .anyRequest().authenticated();//其他任何请求都必须经过身份验证

2. 无访问权限的处理

前面无权限的用户访问时会抛出默认错误面面,并不友好,需要跳转到专门制作的页面,

其实可以直接在springboot默认的静态文件目录下新建一个error文件夹,然后新建一个403.html就行了

3. Thymeleaf 整合 Security

html 页面必须要有判断是否登录的功能,这就需要用到 Thymeleaf 与 Security 的整合包。html 页面必须有判断权限的功能,同样要用到 Thymeleaf 与 Security 的整合包。

导入Thymeleaf 与 Security 的整合包后,就可以在 html 使用 security 标签,这个标签类似大家熟悉的 JSTL 标签具有各种 Security 相关的判断与获取功能。完整过程如下。

(1) 导入 thymeleaf-extras-springsecurity6 依赖,pom.xml 中添加如下代码:

<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity6</artifactId>
</dependency>

(2) 在 index.html 中引入 security 标签。在 index.html 页面的头部<html>标签内部添加如下代码:

<html lang="en"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
>

这样该页面就可以使用 security 标签

(3) 在 index.html 添加如下代码,在页面中添加登录与注销的超链接及登录用户名。

  <div>
      <span sec:authorize="!isAuthenticated()">
          <a th:href="@{/toLogin}">登录</a>
      </span>
  
      &nbsp;&nbsp;
  
      <span sec:authorize="isAuthenticated()">
          <a th:href="@{/logout}">注销</a>
      </span>
      &nbsp;&nbsp;
  
      用户名:<span sec:authentication="name"></span>
  
  </div>
  </br>

<span sec:authorize="isAuthenticated()"></span>表示判断是否登录,如果已经登录,则显示<span></span>标签内部的内容,否则不显示,<spansec:authentication="name"></span>表示获取已经登录的用户名。

(4) 修改各个菜单的 div,通过添加 Security 标签从而根据用户权限决定是否显示该菜单,修改后的有关代码如下所示。

sec:authorize="hasRole('ROLE_role1')" 
sec:authorize="hasRole('ROLE_role2')"
sec:authorize="hasRole('ROLE_role3')"

<div class="menu" sec:authorize="hasRole('ROLE_role1')"> </div>的意思是指如果当前登录用户的 role1 的权限则显示,否则不显示。其中注意角色前面要加上”ROLE_”。

自行测试

使用MybatisPlus实现数据库认证

上面的用户名与密码以及角色与权限是通过在配置类中使用内存方式定义的,实际更多是使用数据库来定义用户与权限的,具体步骤如下。

1. 设计数据库表

创建用户表 user、角色表 role、权限表 menu代表可访问资源,角色权限中间表 menu_role 表示哪个资源有哪些角色可访问,角色与权限是多对多的关系,一个角色可能有多个权限,一个权限可能有多个角色拥有,用户角色中间表 user_role 表示用户有哪些角色,也是多对多关系。预录入一些数据

  • 用户表

  • 角色表

  • 用户角色中间表

  • 菜单表(权限表)

  • 菜单角色中间表

其中角色表需要注意录入的权限需要加上“ROLE_”前缀,之前以内存方式创建角色时并没有加这个前缀是因为系统默认自动添加,但数据库方式不会自动添加,只能手动添加。

2. 导入mybatisPlus和数据库相关依赖

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.4</version>
</dependency>

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

3. 配置 MyBatisPlus 与数据库连接。

在 applciation.properties 中添加如下配置。

server.port=80
spring.datasource.url=jdbc:mysql:///gongfu?serverTimezone=Asia/Shanghai&characterEncoding=utf8
spring.datasource.username=root
spring.datasource.password=123456
mybatis-plus.type-aliases-package=com.jf3q.securitydemo.entity
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

4. 创建实体类。

· 记得加上mybatisPlus的相关注解

@Data
@TableName("userinfo")
public class UserInfo  {

    @TableId(value = "id",type = IdType.AUTO)
    private Integer id;
    private String uname;
    private String pwd;

    @TableField(exist = false)
    private List<Role> roles;
}
@Data
@TableName("userinfo")
public class UserInfo implements UserDetails {

    @TableId(value = "id",type = IdType.AUTO)
    private Integer id;
    private String uname;
    private String pwd;

    @TableField(exist = false)
    private List<Role> roles;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<SimpleGrantedAuthority> list= new ArrayList<>();
        //这个list里面放的就是这个用户的权限
        for (Role role : roles) {
            SimpleGrantedAuthority authority = new SimpleGrantedAuthority(role.getRoleCode());//role1
            list.add(authority);

            for (Menu menu : role.getMenus()) {
                SimpleGrantedAuthority authority1 = new SimpleGrantedAuthority(menu.getMcode());//level1
                list.add(authority1);
            }
        }


        return list;
    }

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

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

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

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

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

    @Override
    public boolean isEnabled() {
        return true;
    }
}
@Data
@TableName(value = "role")
public class Role {
    @TableId(value = "id",type = IdType.AUTO)
    private Integer id;
    private String roleName;

    private String roleCode;
    private List<Menu> menus;
}
@Data
@TableName(value = "menu")
public class Menu {

    @TableId(value = "id",type = IdType.AUTO)
    private Integer id;
    private String mpath;
    private String mcode;

    @TableField(exist = false)
    private List<Role> roles;
}

5. 创建映射文件。

在 resources/mapper 下创建映射文件 UserMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.jf3q.securitydemo.dao.UserMapper">


    <resultMap id="RoleWithMenu" type="role">

        <id property="id" column="id"></id>
        <result property="roleCode" column="role_code"/>
        <result property="roleName" column="role_name"/>

        <collection property="menus" ofType="menu">
            <id property="id" column="mid"></id>
            <result property="mpath" column="mpath"/>
            <result property="mcode" column="mcode"/>
        </collection>
    </resultMap>

    <select id="getRolesByUserId" resultMap="RoleWithMenu">
        SELECT
            r.*, m.id as mid, m.mcode,m.mpath
        FROM
            user_role ur
                LEFT JOIN role r ON r.id = ur.roleid
                LEFT JOIN menu_role mr ON mr.role_id = r.id
                LEFT JOIN menu m ON m.id = mr.menu_id
        WHERE
            ur.userid = #{id}
    </select>
</mapper>

6. 创建业务层 UserService

实现 UserDetailsService 接口,重写 loadUserByUsername方法,实现登录的业务逻辑。

@Service
public class UserService extends ServiceImpl<UserMapper, UserInfo> implements UserDetailsService {
    @Autowired
    UserMapper userMapper;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        LambdaQueryWrapper<UserInfo> queryWrapper= new LambdaQueryWrapper<>();
        queryWrapper.eq(UserInfo::getUname,username);
        List<UserInfo> list = userMapper.selectList(queryWrapper);
        if (list==null || list.size()==0) {
            throw  new UsernameNotFoundException("账号不存在");
        }
        UserInfo userInfo = list.get(0);//剩下一个关键属性roles没赋值


        userInfo.setRoles(userMapper.getRolesByUserId(userInfo.getId()));

        return userInfo;
    }
}

7. 修改springsecurity配置类

注入 UserService 对象,删除原来内存用户配置的方法 userDetailsService,然后在 filterChain 方法中添加一句关键代码:

httpSecurity.userDetailsService(userService);

表示使用上述自定义的业务逻辑类 UserService 进行登录

使用MybatisPlus实现动态授权

现在项目访问什么资源需要什么权限仍然是手动配置的,这样显然不太合适,最好就根据数据库中的数据自动配置访问哪个资源需要哪些角色或权限。

首先要设计好数据库,一个用户可以对应多个角色,一个角色又可对应多个权限,每个权限包含一个可访问的 URL,反过来,每个 URL 可以有多个角色能够访问

Spring Security 提供了 FilterInvocationSecurityMetadataSource 接口,需要在接口的getAttributes 方法中先获取当前用户访问的 URL,再获取数据库权限表 menu 中所有的权限,遍历所有的权限,判断用户的访问的 URL 是否匹配其中一个权限,如果不匹配就放行,如果匹配则获取该权限对应的所有角色,这样就相当于动态的给一个 URL(即当前用户访问的那个 URL)配置了访问它所需要的角色,这些角色存储到 Collection<ConfigAttribute>中。

要想让动态配置的角色起作用,接下来需要实现登录用户的角色与访问当前 URL 所需要的角色进行匹配,这需要用到 AccessDecisionManager 接口,在接口的 decide 方法中,获取上述功能中传递过来的 Collection<ConfigAttribute>,再获取当前登录用户的角色,进行匹配,如果能匹配上则表示允许当前用户访问,否则没有权限访问。

其他更多细节参考如下步骤:

1. 创建 MenuMapper 接口。

接口中创建方法,代码如下:

@Mapper
public interface MenuMapper extends BaseMapper<Menu> {
    List<Menu> getAllMenus();//菜单权限一起
}

2. 创建映射文件 MenuMapper.xml

核心代码如下:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.jf3q.securitydemo.dao.MenuMapper">

    <resultMap id="MenuWithRoles" type="menu">
        <id property="id" column="id"></id>
        <result property="mcode" column="mcode"/>
        <result property="mpath" column="mpath"/>

        <collection property="roles" ofType="role">
            <id property="id" column="rid"></id>
            <result property="roleName" column="role_name"/>
            <result property="roleCode" column="role_code"/>
        </collection>

    </resultMap>

    <select id="getAll" resultMap="MenuWithRoles">

        SELECT
            m.*,  r.id as rid,r.role_code, r.role_name
        FROM
            menu m
                LEFT JOIN menu_role mr ON mr.menu_id = m.id
                LEFT JOIN role r ON r.id = mr.role_id
    </select>
</mapper>

这里的 SQL 代码的含义是通过多表连接查询出所有权限及每个权限对应的角色。

3. 自定义FilterInvocationSecurityMetadataSource类

在 util 包下创建 MyFilterInvocationSecurityMetadataSource 类 , 实现FilterInvocationSecurityMetadataSource 接口,代码如下:

@Component
public class MyFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
    @Autowired
    MenuMapper menuMapper;
    AntPathMatcher antPathMatcher=new AntPathMatcher();
    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {

        //访问此url需要的权限

        //.requestMatchers("/","/loginError","/validcode","/toLogin","/login").permitAll()//匿名访问

        String requestUrl = ((FilterInvocation) object).getRequestUrl();//当前访问的那个url
        if (requestUrl.equals("/")|| requestUrl.equals("/loginError")||requestUrl.equals("/validcode")||requestUrl.equals("/toLogin")||requestUrl.equals("/login")) {
            return null;//访问白名单不需要权限
        }
        List<Menu> list = menuMapper.getAll();
        for (Menu menu : list) {
            //requestUrl   level1/1   /level1/**
            if (antPathMatcher.match(menu.getMpath(),requestUrl)) {
                List<Role> roles = menu.getRoles();//list   里面的role_code转成数组

                String[] roleStr= new String[roles.size()+1];//权限数组 (包含了角色  和菜单)
                for (int i = 0; i < roles.size(); i++) {
                    roleStr[i]=roles.get(i).getRoleCode();
                }

                roleStr[roles.size()]=menu.getMcode();
               return  SecurityConfig.createList(roleStr);
            }


        }


        //level4/8
        return SecurityConfig.createList("ROLE_login");
    }

    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }

    @Override
    public boolean supports(Class<?> clazz) {
        return false;
    }
}

4. 自定义AccessDecisionManager 类

在 util 包下创建 MyAccessDecisionManager 类,实现 AccessDecisionManager 接口中,代码如下:

//当前登陆者有没有上面需要这些权限呢

@Component
public class MyAccessDecisionManager implements AccessDecisionManager {
    @Override
    public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {

        for (ConfigAttribute configAttribute : configAttributes) {

            String need=configAttribute.getAttribute();

            if(need.equals("ROLE_login")){
                if (authentication instanceof AnonymousAuthenticationToken) {
                    throw  new AccessDeniedException("请登录");
                }

                return;//放行
            }

            Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();

            for (GrantedAuthority authority : authorities) {
                if (need.equals(authority.getAuthority())) {
                    return;
                }
            }


        }

        //authentication  当前登陆者的身份信息   里面就有权限



        throw  new AccessDeniedException("没有权限");

        //configAttributes 访问url需要的权限
    }

    @Override
    public boolean supports(ConfigAttribute attribute) {
        return false;
    }

    @Override
    public boolean supports(Class<?> clazz) {
        return false;
    }
}

5. 修改security配置类

在 WebSecurityConfig 中进行配置。注入上述两个类,代码如下:

  @Autowired
    MyAccessDecisionManager myAccessDecisionManager;
    @Autowired
    MyFilterInvocationSecurityMetadataSource myFilterInvocationSecurityMetadataSource;

然后在 filterChain 法中增加如下配置,传递上述对象进去:

 .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                    @Override
                    public <O extends FilterSecurityInterceptor> O postProcess(O object) {

                        object.setSecurityMetadataSource(myFilterInvocationSecurityMetadataSource);//访问此url需要的权限
                        object.setAccessDecisionManager(myAccessDecisionManager);//当前登陆者有没有上面需要这些权限呢
                        return object;
                    }
                })

删除掉 filterChain 方法如下有关手动配置的权限控制有关的代码:

只保留 anyRequest().authenticated(),即所有页面都要经过登录才能访问。

6. 新版动态授权写法

(之前的写法有些方法已经过期,虽然能用但官方已经不推荐了)

参考文章:http://t.csdnimg.cn/VILz1

jwt

什么是jwt

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

JWT令牌的优点

1、jwt基于json,非常方便解析。

2、可以在令牌中自定义丰富的内容,易扩展。

3、通过非对称加密算法及数字签名技术,JWT防止篡改,安全性高。

4、资源服务使用JWT可不依赖认证服务即可完成授权。

令牌结构

JWT令牌由三部分组成,每部分中间使用点(.)分隔,比如:xxxxx.yyyyy.zzzzz

https://jwt.io/

  • Header

头部包括令牌的类型(即JWT)及使用的哈希算法(如HMAC SHA256或RSA)

一个例子如下:

下边是Header部分的内容

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

将上边的内容使用Base64Url编码,得到一个字符串就是JWT令牌的第一部分。

  • Payload

第二部分是负载,内容也是一个json对象,它是存放有效信息的地方,它可以存放jwt提供的现成字段,比

如:iss(签发者),exp(过期时间戳), sub(面向的用户)等,也可自定义字段,比如用户角色。

此部分不建议存放敏感信息,因为此部分可以解码还原原始内容。

最后将第二部分负载使用Base64Url编码,得到一个字符串就是JWT令牌的第二部分。

一个例子:

{
    "sub": "1234567890",
    "name": "456",
    "admin": true
}
  • Signature

第三部分是签名,此部分用于防止jwt内容被篡改。

这个部分使用base64url将前两部分进行编码,编码后使用点(.)连接组成字符串,最后使用header中声明

签名算法进行签名。

一个例子:

HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)

base64UrlEncode(header):jwt令牌的第一部分。

base64UrlEncode(payload):jwt令牌的第二部分。

secret:签名所使用的密钥。

单纯使用jwt

  • jwt依赖
 <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>
        <dependency>
            <groupId>javax.xml.bind</groupId>
            <artifactId>jaxb-api</artifactId>
            <version>2.3.1</version>
        </dependency>
  • jwt工具类
public class JwtUtils {

    public static String getToken(String username){
        return Jwts.builder().setHeaderParam("typ","JWT")
                .setSubject(username)
                .setExpiration(new Date(System.currentTimeMillis()+10*60*1000))
                .signWith(SignatureAlgorithm.HS256,"jf3q.jwt").compact();

    }

    //校验token
    public static Boolean validate(String token){
        try {
            Jwts.parser().setSigningKey("jf3q.jwt").parseClaimsJws(token).getBody();
            return true;
        } catch (Exception e) {
            return  false;
        }


    }

    //获取token中存的用户名
    public static String getUserName(String token){
        Claims body = Jwts.parser().setSigningKey("jf3q.jwt").parseClaimsJws(token).getBody();
        return body.getSubject();

    }
}
  • 测试controller-登录接口
/**
 * @author:xiaojie
 * @create: 2023-04-12 09:44
 * @Description: 登录接口
 */

@RestController
public class LoginController {
    @PostMapping("/login")
    public RestBean login(String username,String password){

        if(username.equals("zhangsan") && password.equals("123456")){
            //生成token  登录成功
            String tokenStr = JwtUtils.getToken(username);
            return RestBean.success("登录成功",tokenStr);
        }else{
            return RestBean.error("账号或者密码错误",null);
        }

    }

}
  • 测试controller-点赞接口
/**
 * @author:xiaojie
 *一般是在请求头里加入Authorization,并加Bearer标注
   Authorization Bearerey JUWVAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJzZWN1cmUtYXBpIiwiYXVkIjoic2VjdXJlLWFwcCIsImV4cCI6MTYwNTQ4MzgwMCwic3ViIjoidXNlciIsImlhdCI6MTYwNTQ4Mzc0MCwicm9sIjpbIlJPTEVfVVNFUiJdfQ.vx0UseLZ62VMfacIrknkreMVVI0h7tIqGRYL5b3seHQ
 * @create: 2023-04-12 10:06
 * @Description: 假设这是一个点赞操作。必须验证用户是否登录状态,,这里也就是验证token是否有效
 */
@RestController
public class UserInfoController {


    //Authorization Bearer

    @GetMapping("/islike")
    public RestBean islike(HttpServletRequest request){

        String authorization = request.getHeader("Authorization");
        String tokenStr = authorization.substring("Bearer".length());

        if(JwtUtils.valicationToken(tokenStr)){
            //该干嘛干嘛   操作数据库  新增点赞记录

            return RestBean.success("点赞成功",tokenStr);
        }else{
            return RestBean.error("token校验失败",null);
        }
    }


}

springsecurity整合jwt

依赖

<!--JWT 依赖-->
 <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>
        <dependency>
            <groupId>javax.xml.bind</groupId>
            <artifactId>jaxb-api</artifactId>
            <version>2.3.1</version>
        </dependency>
<!-- security依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

jwt工具类备用


public class JwtUtils {


    /**
     * 生成token字符串  xxx.yyy.zzzz
     * @param username
     * @return
     */
    public static  String getToken(String username){

        return Jwts.builder().setHeaderParam("typ", "JWT")
                //过期时间  一个小时后此token就失效了
                .setExpiration(new Date(System.currentTimeMillis()+3600000))
                .setSubject(username)
                .signWith(SignatureAlgorithm.HS256, "jf3q-jwt").compact();
    }

    //校验token

    public static Boolean validate(String token ){


        try {
            Jwts.parser().setSigningKey("jf3q-jwt").parseClaimsJws(token).getBody();//拿到jwt令牌里的载荷部分
            return true;
        } catch (ExpiredJwtException e) {
            throw new RuntimeException("token已经过期");
        } catch (UnsupportedJwtException e) {
            throw new RuntimeException("不支持的token");
        } catch (MalformedJwtException e) {
            throw new RuntimeException("token令牌格式不对");
        } catch (SignatureException e) {
            throw new RuntimeException("token签名问题");
        } catch (IllegalArgumentException e) {
            throw new RuntimeException("参数不合法-密钥不对");
        }



    }

    public static String getUserName(String token){

        String username = null;
        try {
            username = Jwts.parser().setSigningKey("jf3q-jwt").parseClaimsJws(token).getBody().getSubject();
        } catch (ExpiredJwtException e) {
            throw new RuntimeException("token已经过期");
        } catch (UnsupportedJwtException e) {
            throw new RuntimeException("不支持的token");
        } catch (MalformedJwtException e) {
            throw new RuntimeException("token令牌格式不对");
        } catch (SignatureException e) {
            throw new RuntimeException("token签名问题");
        } catch (IllegalArgumentException e) {
            throw new RuntimeException("参数不合法-密钥不对");
        }


        return username;

    }
}

测试controller

@RestController
public class UserInfoController {

    @GetMapping("/admin")
    public ResultVo admin(){//管理员这种角色才能访问这个接口

        //相关业务代码省略
        return ResultVo.success("此接口只能是admin角色的人访问",null);
    }

    @GetMapping("/boss")
    public ResultVo boss(){//boss这种角色才能访问这个接口
        //相关业务代码省略
        return ResultVo.success("此接口只能是admin角色的人访问",null);

    }

}

spring security配置类

@Configuration
public class SpringSecurityConfig {

    @Autowired
    MyAccessDeniedHandler MyAccessDeniedHandler;
    @Autowired
    MyAuthenticationEntryPoint myAuthenticationEntryPoint;

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

    @Bean
    UserDetailsService userDetailsService(){
        InMemoryUserDetailsManager manager= new InMemoryUserDetailsManager();


        //考虑到一般数据库存的是密文
        UserDetails user1= User.withUsername("admin").password(passwordEncoder().encode("123")).roles("admin").build();
        UserDetails user2= User.withUsername("boss").password(passwordEncoder().encode("123")).roles("boss").build();
        manager.createUser(user1);
        manager.createUser(user2);
        return manager;
    }
    //白名单
    @Bean
    WebSecurityCustomizer webSecurityCustomizer(){
        return web -> web.ignoring().requestMatchers("/login");
    }
    //配置过滤器链条   授权
    @Bean
    SecurityFilterChain filterChain(HttpSecurity security) throws Exception {
        security.csrf().disable();//前后分离项目必须加这个,否则无法发送post请求

        security
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)//session策略设置成无状态——因为我们是前后分离项目,不用session存登录状态
                .and()
                .authorizeRequests()//如果你使用了authorizeHttpRequests,那么使用withObjectPostProcessor去配置我们自定义的元数据源和权限决策配置时是无效的,因为不会进去
                .requestMatchers("/admin").hasRole("admin")
                .requestMatchers("/boss").hasRole("boss")
                .anyRequest().authenticated()
                .and()
                .exceptionHandling().accessDeniedHandler(MyAccessDeniedHandler)
                .authenticationEntryPoint(myAuthenticationEntryPoint)
                .and()
                .addFilterBefore(new TokenFilter(userDetailsService()), UsernamePasswordAuthenticationFilter.class);
        return security.build();

    }
}

用户登录

@RestController
public class LoginController {


    @Autowired
    UserDetailsService userDetailsService;
    @Autowired
    PasswordEncoder passwordEncoder;


      @PostMapping("/login")
    public Map login(String username,String pass){
        Map map = new HashMap();
        //生成token  返回
        String token = JwtUtils.getToken(username);

        UserDetails userDetails = null;
        try {
            userDetails = userDetailsService.loadUserByUsername(username);
        } catch (UsernameNotFoundException e) {
            map.put("mess","账号不存在");
            return map;
        }

        if(!passwordEncoder.matches(pass,userDetails.getPassword())){
            map.put("mess","密码错误");
            return map;
        }
        //要放到security里面去
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username,pass,userDetails.getAuthorities() );
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);

        map.put("mess","登录成功");
        map.put("token",token);



        return map;

    }
}

token校验的过滤器

特别注意:自定义的过滤器不用放到容器里,否则会出现一个很大的问题:就是login这个接口明明在白名单放行了。但还是会一直进这个自定义的过滤器(校验token,你想想登录接口需要你校验token传没传吗?)

@AllArgsConstructor
public class TokenFilter extends OncePerRequestFilter {

    UserDetailsService userDetailsService;


    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {


        String token = request.getHeader("token");

        //通过token能不能拿到用户名
        String userName = null;
        try {
            userName = JwtUtils.getUserName(token);
        } catch (Exception e) {
            response.setCharacterEncoding("UTF-8");
            PrintWriter out = response.getWriter();
            out.write(JSON.toJSONString(ResultVo.error(406,e.getMessage())));
            out.flush();
            out.close();
            return;
        }
        UserDetails user = userDetailsService.loadUserByUsername(userName);

        if (JwtUtils.validate(token)) {
            //校验成功
            //将用户的凭证存入到security的上下文中
            UsernamePasswordAuthenticationToken authenticationToken= new UsernamePasswordAuthenticationToken(userName,user.getPassword(),user.getAuthorities());
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        }

        filterChain.doFilter(request,response);



    }
}

自定义未授权处理器

@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        //权限不足的情况
        response.setCharacterEncoding("UTF-8");
        PrintWriter out = response.getWriter();
        out.write(JSON.toJSONString(ResultVo.error(403,"权限不足")));
        out.flush();
        out.close();
    }
}

自定义未登录处理器

这个其实可以省略,因为我们在login的那个controller接口里已经判断了账号和密码

@Component
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {

        //认证没通过
        response.setCharacterEncoding("UTF-8");
        PrintWriter out = response.getWriter();
        out.write(JSON.toJSONString(ResultVo.error(401,"请登录")));
        out.flush();
        out.close();
    }
}

测试

自行测试,用postman测试,用boss的账号登录访问不了admin这个接口,用admin的账号登录访问不到boss这个接口

OAuth2基础知识

1.什么是 OAuth2

OAuth 是一个开放标准,该标准允许用户让第三方应用访问该用户在某一网站上存储的私密资源(如头像、照片、视频等),而在这个过程中无须将用户名和密码提供给第三方应用。实现这一功能是通过提供一个令牌(token),而不是用户名和密码来访问他们存放在特定服务提供者的数据。

每一个令牌授权一个特定的网站在特定的时段内访问特定的资源。这样,OAuth 让用户可以授权第三方网站灵活地访问存储在另外一些资源服务器的特定信息,而非所有内容。目前主流的 qq,微信等第三方授权登录方式都是基于 OAuth2 实现的。

OAuth 2 是 OAuth 协议的下一版本,但不向下兼容 OAuth 1.0。OAuth 2 关注客户端开发者的简易性,同时为 Web 应用、桌面应用、移动设备、起居室设备提供专门的认证流程。传统的 Web 开发登录认证一般都是基于 Session 的,但是在前后端分离的架构中继续使用Session 会有许多不便,因为移动端(Android、iOS、微信小程序等)要么不支持 Cookie(微信小程序),要么使用非常不便,对于这些问题,使用 OAuth2 认证都能解决。

2.OAuth2 角色

OAuth2 标准中定义了以下几种角色:

资源所有者(Resource Owner):即代表授权客户端访问本身资源信息的用户,客户端访问用户帐户的权限仅限于用户授权的“范围”

客户端(Client):即代表意图访问受限资源的第三方应用。在访问实现之前,它必须先经过用户者授权,并且获得的授权凭证将进一步由授权服务器进行验证。

授权服务器(Authorization Server):授权服务器用来验证用户提供的信息是否正确,并返回一个令牌给第三方应用。

资源服务器(Resource Server):资源服务器是提供给用户资源的服务器,例如头像、照片、视频等。注意:一般来说,授权服务器和资源服务器可以是同一台服务器

3.OAuth2 授权流程

授权流程图:

第 1 步:客户端(第三方应用)向用户请求授权。

第 2 步:用户单击客户端所呈现的服务授权页面上的同意授权按钮后,服务端返回一个授权许可凭证给客户端。

第 3 步:客户端拿着授权许可凭证去授权服务器申请令牌。

第 4 步:授权服务器验证信息无误后,发放令牌给客户端。

第 5 步:客户端拿着令牌去资源服务器访问资源。

第 6 步:资源服务器验证令牌无误后给予资源。

4.OAuth2 授权模式

OAuth 协议的授权模式共分为 4 种,分别说明如下:客户端 用户 授权服务器 资源服务器。

1.请求授权

2.同意授权

3.申请令牌

4.发放令牌

5.申请资源

6.给予资源

授权码模式:授权码模式(authorizationcode)是功能最完整、流程最严谨的授权模式。它的特点就是通过客户端的服务器与授权服务器进行交互,国内常见的第三方平台登录功能基本都是使用这种模式。

简化模式:简化模式不需要客户端服务器参与,直接在浏览器中向授权服务器中请令牌,一般若网站是纯静态页面,则可以采用这种方式。

密码模式:密码模式是用户把用户名密码直接告诉客户端,客户端使用这些信息向授权服务器中请令牌。这需要用户对客户端高度信任,例如客户端应用和服务提供商是同一家公司。

客户端模式:客户端模式是指客户端使用自己的名义而不是用户的名义向服务提供者申请授权。严格来说,客户端模式并不能算作 OAuth 协议要解决的问题的一种解决方案,但是,对于开发者而言,在一些前后端分离应用或者为移动端提供的认证授权服务器上使用这种模式还是非常方便的。

OAuth2第三方Gitee和GitHub登录实战

即将开发的一个应用(在 Oauth2 中称为客户端)需要获得访问用户的基本信息,除了自行设置注册登录外,假设该用户同时在流行的第三方如 Github、QQ、微信等有账号,还可以引导用户去第三方登录,登录后客户端获得授权码,再去第三方认证服务器申请 token,再凭 token 与第三方获取用户的基本信息为本身应用所用。

这里的第三方使用的是 GitHub,授权模式采用授权码模式。关键步骤如下:

(1) 首先要到 GitHub和gitee上 申请使用第三方认证。

GitHub申请网址:https://github.com/settings/applications/new

gitee申请地址:https://gitee.com/oauth/applications

(2)添加依赖

 <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-oauth2-client</artifactId>
        </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>

(3) 创建控制器,用于设置首页的访问 URL 和登录后的主页的 URL

@Controller
public class MainController {

    @GetMapping("/")
    public String index() {//SpringBoot会自动在方法的参数中注入代表用户的Authentication对象
        return "index.html";
    }

    //登录成功之后的处理
    @GetMapping("/main")
    public void main(OAuth2AuthenticationToken token, HttpServletResponse response) throws IOException {//SpringBoot会自动在方法的参数中注入代表用户的Authentication对象
        System.out.println(String.valueOf(token));
        // 4、输出用户信息
        response.setContentType("text/html;charset=utf-8");
        response.getWriter().write("<h1>登录用户主页</h1>");
        response.getWriter().write(String.valueOf("欢迎你,gitee用户:"+token.getPrincipal().getAttributes().get("login")));

        //第一次登陆的话,就要把信息注册到数据库并登陆成功,可以到个人中心
        //如果不是第一次登录的话,就直接到个人中心呗
    }

}

其中 main 方法代表登录后的主页,获取用户的信息并显示到页面上。

(4) application.yml配置

ClientRegistration 对象实现 OAuth2 客户端和授权服务器之间的链接,这需要用到 SpringSecurity 提供的 ClientRegistration 接口,表示 OAuth2 架构中的客户端与认证服务器之间的详细配置,其中包括:

  • 客户端 ID 和密钥
  • 用于身份验证的授权类型
  • 重定向 URI
spring:
  security:
    oauth2:
      client:
        registration:
          github:
            clientId: ecdefb4c817ccd26387e # 填入自己应用的clientId
            clientSecret: 425265e3c3a503084840fa9eb79acadd76ae42ee # 填入自己应用的clientSecret
            redirectUri: http://localhost/login/oauth2/code/github
          gitee:
            clientId: b3115f19cfe05d4aa78ed0458e830eb84755560db68d3cf31c176fa17dc08d3f # 填入自己应用的clientId
            clientSecret: be519372759072ad261177d994b89e9ada6fc2e8ab5f6f526c31640c5638d1b5 # 填入自己应用的clientSecret
            redirect-uri: http://localhost/login/oauth2/code/gitee
            authorizationGrantType: authorization_code
        provider:
          gitee:
            authorizationUri: https://gitee.com/oauth/authorize
            tokenUri: https://gitee.com/oauth/token
            userInfoUri: https://gitee.com/api/v5/user
            userNameAttribute: name
server:
  port: 80

其中:

  • authorizationUri授权 URI:客户端将用户重定向到其进行身份验证的 URI。
  • tokenUri令牌 URI:客户端为获取访问令牌和刷新令牌而调用的 URI。
  • userInfoUri用户信息 URI:客户但在获得访问令牌后可以调用的 URI,以获得关于用户的更多详细信息。

(5) springsecurity配置类

@Configuration
public class ProjectConfig {
    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {

        ///*.html把static下的html放行掉,不然都被security保护起来了
        //忽略这些静态资源(不拦截)  新版本 Spring Security 6.0 已经弃用 antMatchers()
        return (web) -> web.ignoring().requestMatchers("/*.html","/js/**", "/css/**","/images/**");
    }

    @Bean
    SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
        httpSecurity.authorizeRequests()
                .requestMatchers("/").permitAll()
                .anyRequest().authenticated();//其他任何请求都必须经过身份验证
        //设置身份验证方法
        httpSecurity.oauth2Login().successHandler(new AuthenticationSuccessHandler() {
            @Override
            public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
                response.sendRedirect("/main");
            }
        });
        return httpSecurity.build();
    }


}

作业

要求:

把功夫小demo

  • 前后端不分离做一遍,前端页面html+thymeleaf 后端用springboot+mybatisPlus+security
  • 然后前后端分离再做一遍,前端用vue3写,后端用springboot+mybatisPlus+security
  • 把第三方gitee登录和咱们自己系统内部的登录一起整合进去。提醒:通过第三方登录进来的初始权限都是小师弟级别(一年级)


好博客就要一起分享哦!分享海报

此处可发布评论

评论(0展开评论

暂无评论,快来写一下吧

展开评论

您可能感兴趣的博客

客服QQ 1913284695