SpringSecurity使用

1.认证功能

1.1默认用户登录

依赖导入

<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.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

使用后登录显示security的登陆页面

1.2自定义用户名与密码

yml中设置

spring:
application:
name: security-book

security:
user:
name: admin
password: 123456

或者在WebSecurityConfig中配置

package com.example.securitybook.config;

import com.example.securitybook.filter.ValidCodeFilter;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.configurers.provisioning.InMemoryUserDetailsManagerConfigurer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.core.AuthenticationException;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import java.io.IOException;
import java.io.PrintWriter;

@Configuration
public class WebSecurityConfig {

    @Autowired
    private ValidCodeFilter validCodeFilter;

    @Bean
    PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
        //如果不想加密就返回
        //return NoOpPasswordEncoder.getInstance();
    }

    @Bean
    public UserDetailsService userDetailsService(){
        //使用内存数据进行认证
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        //创建4个用户
        UserDetails user1 = User.withUsername("admin").password(passwordEncoder().encode("123"))
        .roles("role1","role2","role3").build();
        UserDetails user2 = User.withUsername("user2").password(passwordEncoder().encode("123"))
        .roles("role1").build();
        UserDetails user3 = User.withUsername("user3").password(passwordEncoder().encode("123"))
        .roles("role2").build();
        UserDetails user4 = User.withUsername("user4").password(passwordEncoder().encode("123"))
        .roles("role3").build();

        manager.createUser(user1);
        manager.createUser(user2);
        manager.createUser(user3);
        manager.createUser(user4);
        return manager;
    }
}

1.3访问控制

1.同样在WebSecurityConfig中配置

@Bean
public WebSecurityCustomizer webSecurityCustomizer(){
    //忽略这些静态资源
    return(web -> web.ignoring().requestMatchers("/js/**","/css/**","/images/**"));
}

2.WebSecurityConfig中添加filterChain方法,开启登录配置

@Bean
SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception{
    //开启登陆配置
    httpSecurity.authorizeHttpRequests()
    //允许直接访问
    .requestMatchers("/","/index","/validcode")
    .permitAll()
    //其他任何请求都必须经过身份验证
    .anyRequest()
    .authenticated();
    //开启表单验证
    httpSecurity.formLogin();
    return httpSecurity.build();
}

1.4自定义登陆界面与注销登录

1.创建登录页面login.html

2.在Controller中添加登录页面的访问路径

@GetMapping("/toLogin")
public String tologin(){
    return "login";
}

3.在WebSecurityConfig中配置

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

4.实现注销,在WebSecurityConfig中配置

//开启注销
httpSecurity.logout().logoutSuccessUrl("/index");
//关闭csrf
httpSecurity.csrf().disable();

完整配置

package com.example.securitybook.config;

import com.example.securitybook.filter.ValidCodeFilter;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.configurers.provisioning.InMemoryUserDetailsManagerConfigurer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.core.AuthenticationException;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import java.io.IOException;
import java.io.PrintWriter;

@Configuration
public class WebSecurityConfig {

    @Autowired
    private ValidCodeFilter validCodeFilter;

    @Bean
    PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
        //如果不想加密就返回
        //return NoOpPasswordEncoder.getInstance();
    }

    @Bean
    public UserDetailsService userDetailsService(){
        //使用内存数据进行认证
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        //创建4个用户
        UserDetails user1 = User.withUsername("admin").password(passwordEncoder().encode("123"))
        .roles("role1","role2","role3").build();
        UserDetails user2 = User.withUsername("user2").password(passwordEncoder().encode("123"))
        .roles("role1").build();
        UserDetails user3 = User.withUsername("user3").password(passwordEncoder().encode("123"))
        .roles("role2").build();
        UserDetails user4 = User.withUsername("user4").password(passwordEncoder().encode("123"))
        .roles("role3").build();

        manager.createUser(user1);
        manager.createUser(user2);
        manager.createUser(user3);
        manager.createUser(user4);
        return manager;
    }
    @Bean
    public WebSecurityCustomizer webSecurityCustomizer(){
        //忽略这些静态资源
        return(web -> web.ignoring().requestMatchers("/js/**","/css/**","/images/**"));
    }

    @Bean
    SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception{
        httpSecurity.addFilterBefore(validCodeFilter, UsernamePasswordAuthenticationFilter.class);
        //开启登陆配置
        httpSecurity.authorizeHttpRequests()
                //允许直接访问
                .requestMatchers("/","/index","/validcode")
                .permitAll()
                //其他任何请求都必须经过身份验证
                .anyRequest()
                .authenticated();
        //开启表单验证
        httpSecurity.formLogin().loginPage("/toLogin")//跳转到自定义的登录页面
                .usernameParameter("username")//自定义表单的用户名的name,默认为username
                .passwordParameter("password")//自定义表单的密码的name,默认为password
                .loginProcessingUrl("/login")//表单请求的地址,使用Security定义好的/login,并且与自定义表单的action一致
                //.failureUrl("/toLogin/error")//如果登录失败跳转到
                .permitAll()//允许访问登录有关的路径
                .failureHandler(new AuthenticationFailureHandler() {
                    @Override
                    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
                        response.setContentType("application/json;charset = utf-8");
                        PrintWriter out = response.getWriter();
                        String json = "{\"status\":\"error\",\"msg\":\""+exception.getMessage()+"\"}";
                        out.write(json);
                    }
                });

        //开启注销
        httpSecurity.logout().logoutSuccessUrl("/index");
        //关闭csrf
        httpSecurity.csrf().disable();
        //记住我
        httpSecurity.rememberMe();
        return httpSecurity.build();
    }


}

1.5登录认证失败处理

1.在表单验证那里添加.failureUrl("/toLogin/error")

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

2.在控制器中添加方法,对应上述的URL

@GetMapping("/toLogin/error")
public String toLogin(HttpServletRequest request, Model model){
    AuthenticationException authenticationException = (AuthenticationException) request.getSession().getAttribute(WebAttributes.AUTHENTICATION_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 CredentialsExpiredException){
        model.addAttribute("msg","证书过期");
    }
    return "login";
}

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

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

完整前端代码

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
  <head>
    <meta charset="UTF-8">
    <title>login</title>
  </head>
  <body>
    <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="text" name="code"><img src="/validcode" width="100" height="40"/><br/>
      记住我:<input type="checkbox" name="remember-me"/><br/>
      <input type="submit" value="登录"><br/>
      <span th:text="${msg}" style="color:red"></span><br/>
    </form>
  </body>
</html>

1.6记住用户名

1.在配置类中的filterChain方法下添加代码

//记住我
httpSecurity.rememberMe();

2.修改login.html,添加“记住我”选择框,注意name属性必须是remember-me

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

1.7图形验证码的使用

1.创建工具类ValidCode,用于生成验证码

package com.example.securityBook.util;

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Random;

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.创建验证码过滤器

@Component
public class ValidCodeFilter extends GenericFilterBean {

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;
        //下面代码表只过滤/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");
            }
        }
        //如果验证码相同就放行
        chain.doFilter(request,response);
    }
}

3.在Controller中创建方法接收验证码

@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
    session.setAttribute("validcode",validcode);
    //输出图片
    validCode.output(image,response.getOutputStream());
}

2.授权功能

2.1自定义用户授权

1.打开配置类,修改filterChain中的httpSecurity.authorizeHttpRequests()

//开启登陆配置
httpSecurity.authorizeHttpRequests()
//允许直接访问
.requestMatchers("/","/index","/validcode").permitAll()
//用户需要拥有role1的角色才能访问/menu1/**
.requestMatchers("/menu1/**").hasRole("role1")
.requestMatchers("/menu2/**").hasRole("role2")
.requestMatchers("/menu3/**").hasRole("role3")
//其他任何请求都必须经过身份验证
.anyRequest().authenticated();

2.2无访问权限的处理

1.创建errorRole.html界面

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<div>
    <h1>你没有权限访问此页</h1>
    <a th:href="@{/toLogin}">登录</a> &nbsp; <a th:href="@{/index}">返回首页</a>
</div>
</body>
</html>

2.在控制器中添加方法

@GetMapping("/errorRole")
public String errorRole(){
    return "errorRole";
}

3.在配置类的filterChain方法中添加

//没有权限时跳转页
httpSecurity.exceptionHandling().accessDeniedPage("/errorRole");

2.3Thymeleaf整合Security

1.导入依赖

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

2.在index.html文件中引入security标签

<html lang="en"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/extras/spring-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>

4.修改各个菜单的div,通过添加Security标签从而根据用户权限决定是否显示

<div class="menu" sec:authorize="hasRole('ROLE_role1')">
    <h2>menu1</h2>
    <a th:href="@{/menu1/1}">menu1_1</a><br/>
    <a th:href="@{/menu1/2}">menu1_2</a><br/>
    <a th:href="@{/menu1/3}">menu1_3</a><br/>
</div>
<div class="menu" sec:authorize="hasRole('ROLE_role2')">
    <h2>menu2</h2>
    <a th:href="@{/menu2/1}">menu2_1</a><br/>
    <a th:href="@{/menu2/2}">menu2_2</a><br/>
    <a th:href="@{/menu2/3}">menu2_3</a><br/>
</div>
<div class="menu" sec:authorize="hasRole('ROLE_role3')">
    <h2>menu3</h2>
    <a th:href="@{/menu3/1}">menu3_1</a><br/>
    <a th:href="@{/menu3/2}">menu3_2</a><br/>
    <a th:href="@{/menu3/3}">menu3_3</a><br/>
</div>

完整代码

<!DOCTYPE html>
<html lang="en"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
>
<head>
    <meta charset="UTF-8">
    <title>index</title>
    <style>
        .menu{
            width:200px;
            height: 150px;
            border:1px solid #ccc;
            margin-right: 20px;
            float:left;
            text-align: center;
        }


    </style>
</head>
<body>
<h1>主页</h1>
<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>
<div class="menu" sec:authorize="hasRole('ROLE_role1')">
    <h2>menu1</h2>
    <a th:href="@{/menu1/1}">menu1_1</a><br/>
    <a th:href="@{/menu1/2}">menu1_2</a><br/>
    <a th:href="@{/menu1/3}">menu1_3</a><br/>
</div>
<div class="menu" sec:authorize="hasRole('ROLE_role2')">
    <h2>menu2</h2>
    <a th:href="@{/menu2/1}">menu2_1</a><br/>
    <a th:href="@{/menu2/2}">menu2_2</a><br/>
    <a th:href="@{/menu2/3}">menu2_3</a><br/>
</div>
<div class="menu" sec:authorize="hasRole('ROLE_role3')">
    <h2>menu3</h2>
    <a th:href="@{/menu3/1}">menu3_1</a><br/>
    <a th:href="@{/menu3/2}">menu3_2</a><br/>
    <a th:href="@{/menu3/3}">menu3_3</a><br/>
</div>

</body>
</html>

3.使用MyBatis实现数据库认证

1.创建数据库表

2.导入相关依赖

<dependencies>
  <dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>3.0.3</version>
  </dependency>
  <dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.27</version>
  </dependency>
</dependencies>

3.配置数据库链接

spring:
application:
name: security3-book
security:
user:
name: admin
password: 123456

datasource:
username: root
url: jdbc:mysql://localhost:3306/securitydb?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC&useSSL=true
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
type-aliases-package: com.example.securityBook.domain
mapper-locations: classpath:mappers/*.xml
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl


server:
port: 8082

4.实体类

package com.example.securityBook.domain;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.List;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Menu {
    private Integer mid;//权限编号
    private String menuname;//权限名称
    private String url; //该权限对应能访问的URL
    private List<Role> roles; //拥有该权限的所有角色
}
package com.example.securityBook.domain;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.List;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Role {
    private int rid; //角色编号
    private String name; //角色英文名称
    private String nameChinese; //角色中文名称
    private List<Menu> menus;//一个角色可能有多个权限(菜单)
}
package com.example.securityBook.domain;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.Collection;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class TUser{
    private int uid;
    private String username;
    private String password;
    private Collection<Role> roles;//当前用户具有的角色
}

4.创建mapper

package com.example.securityBook.mapper;


import com.example.securityBook.domain.Role;
import com.example.securityBook.domain.TUser;
import org.apache.ibatis.annotations.Mapper;

import java.util.List;

@Mapper
public interface UserMapper {
    TUser findUserByName(String username); //根据姓名查找用户User
    List<Role> findRolesByUserId(int id); //根据用户id查找角色Role集合,角色中又包含权限
}
<?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.example.securityBook.mapper.UserMapper">
  <select id="findUserByName" resultType="TUser">
    select * from user where username=#{username}
  </select>

  <select id="findRolesByUserId" resultMap="roleMap">
    select role.*,menu.* from role,user_role,menu,role_menu
    where role.rid=user_role.rid and role.rid=role_menu.rid and role_menu.mid=menu.mid and uid=#{id}
  </select>

  <resultMap id="roleMap" type="Role">
    <id column="rid" property="rid"/>
    <result column="name" property="name"/>
    <result column="nameChinese" property="nameChinese"/>
    <collection property="menus" ofType="Menu">
      <id column="mid" property="mid"/>
      <result column="menuname" property="menuname"/>
      <result column="url" property="url"/>
    </collection>
  </resultMap>
</mapper>

5.配置UserService,实现UserDetailsService接口,重写loadUserByUsername方法

package com.example.securityBook.service;

import com.example.securityBook.domain.Menu;
import com.example.securityBook.domain.Role;
import com.example.securityBook.domain.TUser;
import com.example.securityBook.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
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.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.Collection;

@Service
public class UserService implements UserDetailsService {
    @Autowired
    UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        TUser tuser=userMapper.findUserByName(username);//从数据库中查询用户
        if(tuser==null){
            throw new UsernameNotFoundException("帐户不存在");
        }
        tuser.setRoles(userMapper.findRolesByUserId(tuser.getUid()));//从数据库中查询到该用户的所有角色(含权限)
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        for(Role role:tuser.getRoles()){//取出用户的角色,封装到authorities中
            authorities.add(new SimpleGrantedAuthority(role.getName()));
            for(Menu menu:role.getMenus()){ //如果权限精确到权限菜单级别,则要补充这个
                authorities.add(new SimpleGrantedAuthority(menu.getMenuname()));
            }
        }
        //下面的密码一般要加密
        return new User(tuser.getUsername(),new BCryptPasswordEncoder().encode(tuser.getPassword()),authorities);

    }
}

6.在配置类中注入UserService对象,在filterChain中添加关键代码

@Autowired
private UserService userService;
//数据库认证
httpSecurity.userDetailsService(userService);

完整代码如下

package com.example.securityBook.config;




import com.example.securityBook.filter.ValidCodeFilter;
import com.example.securityBook.service.UserService;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.core.AuthenticationException;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import java.io.IOException;
import java.io.PrintWriter;

@Configuration
public class WebSecurityConfig {

    @Autowired
    private ValidCodeFilter validCodeFilter;

    @Autowired
    private UserService userService;

    @Bean
    PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
        //如果不想加密就返回
        //return NoOpPasswordEncoder.getInstance();
    }


    @Bean
    public WebSecurityCustomizer webSecurityCustomizer(){
        //忽略这些静态资源
        return(web -> web.ignoring().requestMatchers("/js/**","/css/**","/images/**"));
    }

    @Bean
    SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception{
        httpSecurity.addFilterBefore(validCodeFilter, UsernamePasswordAuthenticationFilter.class);
        //开启登陆配置
        httpSecurity.authorizeHttpRequests()
                //允许直接访问
                .requestMatchers("/","/index","/validcode").permitAll()
                //用户需要拥有role1的角色才能访问/menu1/**
                .requestMatchers("/menu1/**").hasRole("user")
                .requestMatchers("/menu2/**").hasRole("manager")
                .requestMatchers("/menu3/**").hasRole("admin")
                //其他任何请求都必须经过身份验证
                .anyRequest().authenticated();
        //开启表单验证
        httpSecurity.formLogin().loginPage("/toLogin")//跳转到自定义的登录页面
                .usernameParameter("username")//自定义表单的用户名的name,默认为username
                .passwordParameter("password")//自定义表单的密码的name,默认为password
                .loginProcessingUrl("/login")//表单请求的地址,使用Security定义好的/login,并且与自定义表单的action一致
                //.failureUrl("/toLogin/error")//如果登录失败跳转到
                .permitAll()//允许访问登录有关的路径
                .failureHandler(new AuthenticationFailureHandler() {
                    @Override
                    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
                        response.setContentType("application/json;charset = utf-8");
                        PrintWriter out = response.getWriter();
                        String json = "{\"status\":\"error\",\"msg\":\""+exception.getMessage()+"\"}";
                        out.write(json);
                    }
                });

        //开启注销
        httpSecurity.logout().logoutSuccessUrl("/index");
        //关闭csrf
        httpSecurity.csrf().disable();
        //记住我
        httpSecurity.rememberMe();
        //没有权限时跳转页
        httpSecurity.exceptionHandling().accessDeniedPage("/errorRole");
        //数据库认证
        httpSecurity.userDetailsService(userService);
        return httpSecurity.build();
    }


}

注:index.html中有段角色名称也要进行相关修改,如ROLE_user替换ROLE_role1

4.使用MyBatis实现动态授权

因为访问什么资源有什么权力都是要我们手动设置的,所以我们直接从数据库查询每个角色有什么权限,将这些角色存储到Collection<ConfigAttribute>中

1.创建MenuMapper接口

package com.example.securityBook.mapper;

import com.example.securityBook.domain.Menu;
import org.apache.ibatis.annotations.Mapper;

import java.util.List;

@Mapper
public interface MenuMapper {

    //找到全部权限的集合
    List<Menu> getAllMenus();
}
<?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.example.securityBook.mapper.MenuMapper">
    <resultMap id="menuMap" type="Menu">
        <id column="mid" property="mid"></id>
        <result column="menuname" property="menuname"></result>
        <result column="url" property="url"></result>
        <collection property="roles" ofType="Role">
            <id column="rid" property="rid"></id>
            <result column="name" property="name"></result>
            <result column="nameChinese" property="nameChinese"></result>
        </collection>
    </resultMap>
    <select id="getAllMenus" resultMap="menuMap">
        select menu.*,role.* from menu,role_menu,role where menu.mid=role_menu.mid and role_menu.rid=role.rid
    </select>
</mapper>

2.创建工具类MyFilterInvocationSecurityMetadataSource类

package com.example.securityBook.util;


import com.example.securityBook.domain.Menu;
import com.example.securityBook.domain.Role;
import com.example.securityBook.mapper.MenuMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;

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

@Component
public class MyFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
    AntPathMatcher antPathMatcher=new AntPathMatcher();
    @Autowired
    MenuMapper menuMapper;

    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {

        String requestUrl=((FilterInvocation)object).getRequestUrl();//获取当前访问路径
        List<Menu> menus=menuMapper.getAllMenus(); //获取所有的权限,每个权限又包括多个角色
        for(Menu menu:menus){
            if(antPathMatcher.match(menu.getUrl(),requestUrl)){ //用户访问的URL与数据库中的权限匹配
                List<Role> roles=menu.getRoles(); //获取该权限对应的所有角色
                String[] roleArr=new String[roles.size()];//角色集合转化为数组
                for(int i=0;i<roles.size();i++){
                    roleArr[i]=roles.get(i).getName();
                }
                return SecurityConfig.createList(roleArr);//角色数组转换为Collection<ConfigAttribute>返回
            }
        }
        return null;
    }

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

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

3.创建工具类MyAccessDecisionManager类

package com.example.securityBook.util;

import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Component;

import java.util.Collection;

@Component
public class MyAccessDecisionManager implements AccessDecisionManager {
    @Override
    public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
        Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
        for(ConfigAttribute configAttribute:configAttributes){ //遍历当前url所需的全部角色
            if(configAttribute.getAttribute()==null && authentication instanceof UsernamePasswordAuthenticationToken){
                return; //表示已经认证过的
            }

            for(GrantedAuthority authority:authorities){ //遍历当前用户所有的角色权限,如果有一个能匹配上则成功
                if(configAttribute.getAttribute().equals(authority.getAuthority())){
                    return;
                }
            }
        }
        throw new AccessDeniedException("没有权限");

    }

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

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

4.在配置类中进行配置,注入以上两个类

@Autowired
private MyFilterInvocationSecurityMetadataSource myFilterInvocationSecurityMetadataSource;

@Autowired
private MyAccessDecisionManager myAccessDecisionManager;

5.修改配置类中filterChain方法中的配置

//开启登陆配置
        httpSecurity.authorizeHttpRequests()
                //允许直接访问
                .requestMatchers("/","/index","/validcode").permitAll()
                //用户需要拥有role1的角色才能访问/menu1/**
//                .requestMatchers("/menu1/**").hasRole("user")
//                .requestMatchers("/menu2/**").hasRole("manager")
//                .requestMatchers("/menu3/**").hasRole("admin")
                .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                    @Override
                    public <O extends FilterSecurityInterceptor> O postProcess(O object) {
                        object.setSecurityMetadataSource(myFilterInvocationSecurityMetadataSource);
                        object.setAccessDecisionManager(myAccessDecisionManager);
                        return object;
                    }
                })
                //其他任何请求都必须经过身份验证
                .anyRequest().authenticated();

完整代码

package com.example.securityBook.config;




import com.example.securityBook.filter.ValidCodeFilter;
import com.example.securityBook.service.UserService;
import com.example.securityBook.util.MyAccessDecisionManager;
import com.example.securityBook.util.MyFilterInvocationSecurityMetadataSource;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.core.AuthenticationException;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import java.io.IOException;
import java.io.PrintWriter;

@Configuration
public class WebSecurityConfig {

    @Autowired
    private MyFilterInvocationSecurityMetadataSource myFilterInvocationSecurityMetadataSource;

    @Autowired
    private MyAccessDecisionManager myAccessDecisionManager;
    @Autowired
    private ValidCodeFilter validCodeFilter;

    @Autowired
    private UserService userService;

    @Bean
    PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
        //如果不想加密就返回
        //return NoOpPasswordEncoder.getInstance();
    }


    @Bean
    public WebSecurityCustomizer webSecurityCustomizer(){
        //忽略这些静态资源
        return(web -> web.ignoring().requestMatchers("/js/**","/css/**","/images/**"));
    }

    @Bean
    SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception{
        httpSecurity.addFilterBefore(validCodeFilter, UsernamePasswordAuthenticationFilter.class);
        //开启登陆配置
        httpSecurity.authorizeHttpRequests()
                //允许直接访问
                .requestMatchers("/","/index","/validcode").permitAll()
                //用户需要拥有role1的角色才能访问/menu1/**
//                .requestMatchers("/menu1/**").hasRole("user")
//                .requestMatchers("/menu2/**").hasRole("manager")
//                .requestMatchers("/menu3/**").hasRole("admin")
                .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                    @Override
                    public <O extends FilterSecurityInterceptor> O postProcess(O object) {
                        object.setSecurityMetadataSource(myFilterInvocationSecurityMetadataSource);
                        object.setAccessDecisionManager(myAccessDecisionManager);
                        return object;
                    }
                })
                //其他任何请求都必须经过身份验证
                .anyRequest().authenticated();
        //开启表单验证
        httpSecurity.formLogin().loginPage("/toLogin")//跳转到自定义的登录页面
                .usernameParameter("username")//自定义表单的用户名的name,默认为username
                .passwordParameter("password")//自定义表单的密码的name,默认为password
                .loginProcessingUrl("/login")//表单请求的地址,使用Security定义好的/login,并且与自定义表单的action一致
                //.failureUrl("/toLogin/error")//如果登录失败跳转到
                .permitAll()//允许访问登录有关的路径
                .failureHandler(new AuthenticationFailureHandler() {
                    @Override
                    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
                        response.setContentType("application/json;charset = utf-8");
                        PrintWriter out = response.getWriter();
                        String json = "{\"status\":\"error\",\"msg\":\""+exception.getMessage()+"\"}";
                        out.write(json);
                    }
                });

        //开启注销
        httpSecurity.logout().logoutSuccessUrl("/index");
        //关闭csrf
        httpSecurity.csrf().disable();
        //记住我
        httpSecurity.rememberMe();
        //没有权限时跳转页
        httpSecurity.exceptionHandling().accessDeniedPage("/errorRole");
        //数据库认证
        httpSecurity.userDetailsService(userService);
        return httpSecurity.build();
    }


}

 5.使用注解实现权限控制

1.在配置类中添加以下注解

@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true,jsr250Enabled = true)

2.删除filterChain方法中有关手动配置的权限配置

.requestMatchers("/menu1/**").hasRole("user")
.requestMatchers("/menu2/**").hasRole("manager")
.requestMatchers("/menu3/**").hasRole("admin")

3.在控制类中添加方法,使用@Secured或者@PreAuthorize注解

jpackage com.example.securityBook.controller;


import com.example.securityBook.util.ValidCode;
import jakarta.annotation.security.DenyAll;
import jakarta.annotation.security.PermitAll;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import org.springframework.security.access.annotation.Secured;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.CredentialsExpiredException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.WebAttributes;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.ResponseBody;

import java.awt.image.BufferedImage;
import java.io.IOException;

@Controller
public class SecurityController {

    //首页访问路径
    @GetMapping("/index")
    public String index(){
        return "index";
    }

    @GetMapping("/toLogin")
    public String tologin(HttpServletRequest request){
        String msg = (String) request.getSession().getAttribute("msg");
        if (msg != null){
            request.setAttribute("msg",msg);
        }
        return "login";
    }

    @Secured({"ROLE_user","ROLE_manager","ROLE_admin"}) //有三种角色之一可以访问
    @GetMapping("/menu1/{id}") //示例:访问menu1/1将返回menu1/1.html
    public String menu1(@PathVariable("id")int id){
        return "menu1/"+id;
    }

    @PreAuthorize("hasAnyRole('manager','admin')") //有二种角色之一可以访问
    @GetMapping("/menu2/{id}") //示例:访问menu1/1将返回menu2/1.html
    public String menu2(@PathVariable("id")int id){
        return "menu2/"+id;
    }

    @PreAuthorize("hasRole('admin')") //有admin角色可以访问
    @GetMapping("/menu3/{id}") //示例:访问menu1/1将返回menu3/1.html
    public String menu3(@PathVariable("id")int id){
        return "menu3/"+id;
    }

    @ResponseBody
    @PreAuthorize("hasAuthority('商品管理')") //有商品管理权限可以访问
    @GetMapping("/test1")
    public String test1(){
        return "商品管理hasAuthority";
    }

    @ResponseBody
    @PreAuthorize("hasAuthority('用户管理')") //有用户管理权限可以访问
    @GetMapping("/test2")
    public String test2(){
        return "用户管理hasAuthority";
    }


    @ResponseBody
    @DenyAll
    @GetMapping("/test3") //所有角色都不可访问
    public String test3(){
        return "DenyAll";
    }

    @ResponseBody
    @PermitAll
    @GetMapping("/test4") //所有角色都可访问
    public String test4(){
        return "PermitAll";
    }

    /*//只需要有3中角色之一便可以访问
    @Secured({"ROLE_user","ROLE_manager","ROLE_admin"})
    //示例,访问menu/1,将返回menu1/1.html
    @GetMapping("/menu{num}/{id}")
    public String menu(@PathVariable("num") int num,@PathVariable("id") int id){
        return "menu"+num+"/"+id;
    }*/

    @GetMapping("/toLogin/error")
    public String toLogin(HttpServletRequest request, Model model){
        AuthenticationException authenticationException = (AuthenticationException) request.getSession().getAttribute(WebAttributes.AUTHENTICATION_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 CredentialsExpiredException){
            model.addAttribute("msg","证书过期");
        }
        return "login";
    }

    @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
        session.setAttribute("validcode",validcode);
        //输出图片
        validCode.output(image,response.getOutputStream());
    }

    @GetMapping("/errorRole")
    public String errorRole(){
        return "errorRole";
    }
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值