从零整合SpringBoot + SpringSecurity + Thymeleaf入门案例(附源码),一步一步手把手构建。零基础小白也可快速上手,实习找工作利器。

场景描述

最经在学习 SpringSecurity安全框架。记录一下如何从零将SpringSecuritySpringBoot一步一步构建进去。大概的搭建步骤是:

  1. 首先,搭建一个纯SpringBoot框架搭建项目进行浏览器页面访问。会详细记录搭建流程。
  2. 第二,完成纯框架搭建后便是简单整合SpringBoot + SpringSecurity后的效果。
  3. 第三,简单整合后便是SpringBoot + SpringSecurity + Thymeleaf整合。这一步会整合进去前端页面效果。

描述:分步骤进行整合的目的就是能够更直观的对比整合前后的效果。方便日后可以更快速的进行框架的搭建。

一、纯 SpringBoot 框架搭建项目

  1. idea快发工具构建一个SpringBoot项目,构建步骤如下截图。
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    注意:点击完成后会idea工具会使用maven自动创建项目。但是有时候pom爆红。有可能是以下几个原因。得自己设置好maven仓库,最好在配置文件配置好阿里云的镜像仓库。还有就是idea工具缓存的问题,导致无法加载到自己需要的SpringBoot版本。解决办法就是清除缓存加重启idea。最后都没用的话可以试着去maven仓库找到有的版本进行依赖的配置。项目构建完没问题的如下图所示。
    在这里插入图片描述
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.5</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.study</groupId>
    <artifactId>security_demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>security_demo</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <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>
    </dependencies>

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

</project>

  1. 编写 TestController类。存放路径:com.study.security_demo.controller.TestController
@RestController
public class TestController {
    @RequestMapping("/demo")
    public String hello(){
        return "hello,Spring Security";
    }
}
  1. 启动SpringBoot项目。浏览器访问路径:http://localhost:8080/demo
    在这里插入图片描述

提示:控制台没有报错就说明启动成功。

在这里插入图片描述

上图是正常运行后浏览器的访问结果。这也是没有做权限控制的访问结果。

二、简单整合 SpringBoot + SpringSecurity 步骤

  1. 首先我们需要添加SpringSecuritypom文件的依赖
		<!-- Spring Security依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
  1. 刷新依赖,重启项目。然后我们再去访问http://localhost:8080/demo,会得到如下页面。
    在这里插入图片描述

描述:这时SpringSecurity会直接接过访问权限。重定向到它自定的一个页面,让我们登陆。有默认的用户名user、密码是控台输出的一段由uuid生成的字符序列。如下图所示:

在这里插入图片描述
源码的是在这个类:org.springframework.boot.autoconfigure.security.SecurityProperties
在这里插入图片描述

描述:这是你输入用户名跟密码就可以访问刚才的资源了。比如我的是:
用户名:user
密码:a0e69fbe-abe5-4e9d-beba-86cf7df70b13
你想知道你的就去idea控制台找到那段字符序列输入即可。

  1. 上一步是按照默认的方式,如果我们想要配置自己的用户名跟密码进行登陆限制,那就在resources文件夹下的application.properties配置文件配置如下内容即可。
# 通过配置文件设置可以登陆的用户名跟密码
spring.security.user.name=admin
spring.security.user.password=123456

在这里插入图片描述

描述配置完后就可以用你自己的设置的用户名密码进行登陆了。这便是简单的整合SpringBoot + SpringSecurity

三、SpringBoot + SpringSecurity + Thymeleaf 整合

描述:这一步主要是为了整合进去html,做到对访问路径的访问控制。也就是说不同的登陆用户对于相同的页面有不同的访问权限。方便更好的理解SpringSecurity权限控制。

  1. 首先我们需要先引入用到的相关依赖。
		<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>

        <!-- Spring Security依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

 		<!--Thymeleaf整合SpringBoot依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

        <!--对Thymeleaf添加Spring Security标签支持-->
        <dependency>
            <groupId>org.thymeleaf.extras</groupId>
            <artifactId>thymeleaf-extras-springsecurity5</artifactId>
        </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>
  1. 为了方便测试我们先关闭Thymeleaf的缓存。
# 关闭 thymeleaf 引擎缓存
spring.thymeleaf.cache=false

在这里插入图片描述

  1. 整合好相关的资源文件。文件目录如下:
    在这里插入图片描述

描述上述就是要整合的资源文件,我会在下面放入网盘链接,方便各位学习下载使用。拿到文件后直接拷贝到相关目录即可。下面我会分析各个文件的作用。

运行效果:

描述:重启项目。然后我们再去访问:http://localhost:8080
会得到如下页面:
在这里插入图片描述
描述:上图是首页面,有导航栏,有7个超链接。分别对应跳转不同页面。需要登录点击登录
在这里插入图片描述
描述上图是登录页面有4个账户可以登陆,不同的账户对应这不同页面访问权限。
4个账户分别是:a\a、b\b、c\c、d\d。
在这里插入图片描述
描述上图是a账户登录可以访问的页面显示。
在这里插入图片描述
描述无权限的访问页如示
如果没有登录是只能访问首页,访问其他页面都会跳转到登录。
下面看下实现方式。

首先这里定义了五个自定义权限注解,分别是:
  • com.study.security_demo.annotations.IsAdmin
  • com.study.security_demo.annotations.IsEditor
  • com.study.security_demo.annotations.IsReviewer
  • com.study.security_demo.annotations.IsUser
  • com.study.security_demo.annotations.MyPermissions
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasAnyRole('ROLE_ADMIN')")
public @interface IsAdmin {
}

描述:我们拿其中一个注解的源码看下,@Target@Retentionjava元注解。@Target是用于标注该注解使用方式:METHOD 是用于方法上、TYPE 是用于类上。@Retention该注解表示在运行时环境生效。@PreAuthorizeSpringSecurity的注解,只有一个值,表示定义的权限规则。hasAnyRole('ROLE_ADMIN')表示拥有任何一个权限既有效。
我们在开发网站的过程中,比如 GET /user/editor这个请求角色为 EDITORADMIN肯定都可以,如果我们在每一个需要判断权限的方法上面写一长串的权限表达式,一定很复杂。但是通过自定义权限注解,我们可以通过@IsEditor这样的方法来判断,这样一来就简单了很多。

其次,需要继承一个 User 类,定义一个 CustomUser 类。
  • org.springframework.security.core.userdetails.User; 继承该类。
public class CustomUser extends User {

    private int id;

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

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }
}

描述:继承后我们就可以利用这个类定义自己需要的账户,用于访问登录了。

接下来,我是用map集合来模拟数据库数据,定义一个 Database 类。
public class Database {

    private Map<String, CustomUser> data;

    private PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();

    public Database() {
        data = new HashMap<>();

        CustomUser a = new CustomUser(1, "a", getPassword("a"), getGrants("ROLE_USER"));
        CustomUser b = new CustomUser(2, "b", getPassword("b"), getGrants("ROLE_EDITOR"));
        CustomUser c = new CustomUser(3, "c", getPassword("c"), getGrants("ROLE_REVIEWER"));
        CustomUser d = new CustomUser(4, "d", getPassword("d"), getGrants("ROLE_ADMIN"));
        data.put("a", a);
        data.put("b", b);
        data.put("c", c);
        data.put("d", d);
    }

    public Map<String, CustomUser> getDatabase() {
        return data;
    }

    private String getPassword(String raw) {
        return passwordEncoder.encode(raw);
    }

    private Collection<GrantedAuthority> getGrants(String role) {
        return AuthorityUtils.commaSeparatedStringToAuthorityList(role);
    }
}
再接着,需要继承一个 WebSecurityConfigurerAdapter 类,定义一个 SecurityConfig 类。
  • org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 继承该类。
/**
 * 开启方法注解支持,我们设置prePostEnabled = true是为了后面能够使用hasRole()这类表达式
 * 
 */
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * TokenBasedRememberMeServices的生成密钥
     */
    private final String SECRET_KEY = "123456";

    @Autowired
    private CustomUserDetailsService customUserDetailsService;

    /**
     * 必须有此方法,Spring Security官方规定必须要有一个密码加密方式。
     * 注意:例如这里用了BCryptPasswordEncoder()的加密方法,那么在保存用户密码的时候也必须使用这种方法,确保前后一致。
     * 详情参见项目中Database.java中保存用户的逻辑
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 配置Spring Security,下面说明几点注意事项。
     * 1. Spring Security 默认是开启了CSRF的,此时我们提交的POST表单必须有隐藏的字段来传递CSRF,
     * 而且在logout中,我们必须通过POST到 /logout 的方法来退出用户,详见我们的login.html和logout.html.
     * 2. 开启了rememberMe()功能后,我们必须提供rememberMeServices,例如下面的getRememberMeServices()方法,
     * 而且我们只能在TokenBasedRememberMeServices中设置cookie名称、过期时间等相关配置,如果在别的地方同时配置,会报错。
     * 错误示例:xxxx.and().rememberMe().rememberMeServices(getRememberMeServices()).rememberMeCookieName("cookie-name")
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
                .loginPage("/login") // 自定义用户登入页面
                .failureUrl("/login?error") // 自定义登入失败页面,前端可以通过url中是否有error来提供友好的用户登入提示
                .and()
                .logout()
                .logoutUrl("/logout")// 自定义用户登出页面
                .logoutSuccessUrl("/")
                .and()
                .rememberMe() // 开启记住密码功能
                .rememberMeServices(getRememberMeServices()) // 必须提供
                .key(SECRET_KEY) // 此SECRET需要和生成TokenBasedRememberMeServices的密钥相同
                .and()
                /*
                 * 默认允许所有路径所有人都可以访问,确保静态资源的正常访问。
                 * 后面再通过方法注解的方式来控制权限。
                 */
                .authorizeRequests().anyRequest().permitAll()
                .and()
                .exceptionHandling().accessDeniedPage("/403"); // 权限不足自动跳转403
    }

    /**
     * 如果要设置cookie过期时间或其他相关配置,请在下方自行配置
     */
    private TokenBasedRememberMeServices getRememberMeServices() {
        TokenBasedRememberMeServices services = new TokenBasedRememberMeServices(SECRET_KEY, customUserDetailsService);
        services.setCookieName("remember-cookie");
        services.setTokenValiditySeconds(100); // 默认14天
        return services;
    }
}

描述:具体描述可以看注释了。开启方法注解支持:只需要在类上添加 @EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true) 注解,我们设置 prePostEnabled = true 是为了支持 hasRole() 这类表达式。

还有页面类 UserService 类。
@Service
public class UserService {

    private Database database = new Database();

    public CustomUser getUserByUsername(String username) {
        CustomUser originUser = database.getDatabase().get(username);
        if (originUser == null) {
            return null;
        }

        /*
         * 此处有坑,之所以这么做是因为Spring Security获得到User后,会把User中的password字段置空,以确保安全。
         * 因为Java类是引用传递,为防止Spring Security修改了我们的源头数据,所以我们复制一个对象提供给Spring Security。
         * 如果通过真实数据库的方式获取,则没有这种问题需要担心。
         */
        return new CustomUser(originUser.getId(), originUser.getUsername(), originUser.getPassword(), originUser.getAuthorities());
    }
}
还需要继承一个 UserDetailsService 类,定义一个 CustomUserDetailsService 类。
  • org.springframework.security.core.userdetails.UserDetailsService; 继承该类。
/**
 * 实现官方提供的UserDetailsService接口即可
 */
@Service
public class CustomUserDetailsService implements UserDetailsService {

    private Logger LOGGER = LoggerFactory.getLogger(getClass());

    @Autowired
    private UserService userService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        CustomUser user = userService.getUserByUsername(username);
        if (user == null) {
            throw new  UsernameNotFoundException("该用户不存在");
        }
        LOGGER.info("用户名:"+username+" 角色:"+user.getAuthorities().toString());
        return user;
    }
}
在定义三个controller
  • UserController
@IsUser // 表明该控制器下所有请求都需要登入后才能访问
@Controller
@RequestMapping("/user")
public class UserController {

    @GetMapping("/home")
    public String home(Model model) {
        // 方法一:通过SecurityContextHolder获取
        CustomUser user = (CustomUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        model.addAttribute("user", user);
        return "user/home";
    }

    @GetMapping("/editor")
    @IsEditor
    public String editor(Authentication authentication, Model model) {
        // 方法二:通过方法注入的形式获取Authentication
        CustomUser user = (CustomUser) authentication.getPrincipal();
        model.addAttribute("user", user);
        return "user/editor";
    }

    @GetMapping("/reviewer")
    @IsReviewer
    public String reviewer(Principal principal, Model model) {
        // 方法三:同样通过方法注入的方法,注意要转型,此方法很二,不推荐
        CustomUser user = (CustomUser) ((Authentication) principal).getPrincipal();
        model.addAttribute("user", user);
        return "user/reviewer";
    }

    @GetMapping("/admin")
    @IsAdmin
    public String admin() {
        // 方法四:通过Thymeleaf的Security标签进行,详情见admin.html
        return "user/admin";
    }

    @GetMapping("/perm")
    @MyPermissions
    public String perm() {
        // 方法四:通过Thymeleaf的Security标签进行,详情见admin.html
        return "user/perm";
    }
}

描述:用户访问控制器,根据不同访问路径进行html页面的访问。

  • IndexController
@Controller
public class IndexController {

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

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

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

描述:登录访问控制器,主页、登录页、退出页。

  • ErrorController
@Controller
public class ErrorController {

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

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

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

描述:错误页面访问控制器。

最后总结:

    源代码我会上传到csdn,需要的可以直接下载学习。如果对你有帮助的话可以麻烦给个小小的赞,举手之劳便是对博主最大的鼓励,谢谢。
源代码链接

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

博扬java张

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值