关于springboot:Spring-Boot-Spring-Security-Thymeleaf-简单教程

39次阅读

共计 10679 个字符,预计需要花费 27 分钟才能阅读完成。

Spring Security 基本原理

Spring Security 过滤器链

Spring Security 实现了一系列的过滤器链,就依照上面程序一个一个执行上来。

  1. ....class 一些自定义过滤器(在配置的时候你能够本人抉择插到哪个过滤器之前),因为这个需要因人而异,本文不探讨,大家能够本人钻研
  2. UsernamePasswordAithenticationFilter.class Spring Security 自带的表单登入验证过滤器,也是本文次要应用的过滤器
  3. BasicAuthenticationFilter.class
  4. ExceptionTranslation.class 异样解释器
  5. FilterSecurityInterceptor.class 拦截器最终决定申请是否通过
  6. Controller 咱们最初本人编写的控制器

相干类阐明

  • User.class:留神这个类不是咱们本人写的,而是 Spring Security 官网提供的,他提供了一些根底的性能,咱们能够通过继承这个类来裁减办法。详见代码中的 CustomUser.java
  • UserDetailsService.class:Spring Security 官网提供的一个接口,外面只有一个办法loadUserByUsername(),Spring Security 会调用这个办法来获取数据库中存在的数据,而后和用户 POST 过去的用户名明码进行比对,从而判断用户的用户名明码是否正确。所以咱们须要本人实现loadUserByUsername() 这个办法。详见代码中的 CustomUserDetailsService.java

我的项目逻辑

为了体现权限区别,咱们通过 HashMap 结构了一个数据库,外面蕴含了 4 个用户

ID 用户名 明码 权限
1 jack jack123 user
2 danny danny123 editor
3 alice alice123 reviewer
4 smith smith123 admin

阐明下权限

user:最根底的权限,只有是登入用户就有 user 权限

editor:在 user 权限下面减少了 editor 的权限

reviewer:与上同理,editorreviewer 属于同一级的权限

admin:蕴含所有权限

为了测验权限,咱们提供若干个页面

网址 阐明 可拜访权限
/ 首页 所有人均可拜访(anonymous)
/login 登入页面 所有人均可拜访(anonymous)
/logout 退出页面 所有人均可拜访(anonymous)
/user/home 用户核心 user
/user/editor editor, admin
/user/reviewer reviewer, admin
/user/admin admin
/403 403 谬误页面,丑化过,大家能够间接用 所有人均可拜访(anonymous)
/404 404 谬误页面,丑化过,大家能够间接用 所有人均可拜访(anonymous)
/500 500 谬误页面,丑化过,大家能够间接用 所有人均可拜访(anonymous)

代码配置

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 http://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.1.1.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>org.inlighting</groupId>
    <artifactId>security-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>security-demo</name>
    <description>Demo project for Spring Boot &amp; Spring Security</description>

    <!-- 指定 JDK 版本,大家能够改成本人的 -->
    <properties>
        <java.version>11</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- 对 Thymeleaf 增加 Spring Security 标签反对 -->
        <dependency>
            <groupId>org.thymeleaf.extras</groupId>
            <artifactId>thymeleaf-extras-springsecurity5</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- 开发的热加载配置 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>

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

application.properties 配置

为了使热加载(这样批改模板后无需重启 Tomcat)失效,咱们须要在 Spring Boot 的配置文件下面加上一段话

spring.thymeleaf.cache=false

如果须要具体理解热加载,请看官网文档:https://docs.spring.io/spring…

Spring Security 配置

首先咱们开启办法注解反对:只须要在类上增加 @EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true) 注解,咱们设置 prePostEnabled = true 是为了反对 hasRole() 这类表达式。如果想进一步理解办法注解能够看 Introduction to Spring Method Security 这篇文章。

SecurityConfig.java

/**
 * 开启办法注解反对,咱们设置 prePostEnabled = true 是为了前面可能应用 hasRole()这类表达式
 * 进一步理解可看教程:https://www.baeldung.com/spring-security-method-security
 */
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * TokenBasedRememberMeServices 的生成密钥,* 算法实现详见文档:https://docs.spring.io/spring-security/site/docs/5.1.3.RELEASE/reference/htmlsingle/#remember-me-hash-token
     */
    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;
    }
}

UserService.java

本人模仿数据库操作的 Service,用于向本人通过HashMap 模仿的数据源获取数据。

@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());
    }
}

CustomUserDetailsService.java

/**
 * 实现官网提供的 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;
    }
}

自定义权限注解

咱们在开发网站的过程中,比方 GET /user/editor 这个申请角色为 EDITORADMIN 必定都能够,如果咱们在每一个须要判断权限的办法下面写一长串的权限表达式,肯定很简单。然而通过自定义权限注解,咱们能够通过 @IsEditor 这样的办法来判断,这样一来就简略了很多。进一步理解能够看:Introduction to Spring Method Security

IsUser.java

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasAnyAuthority('ROLE_USER','ROLE_EDITOR','ROLE_REVIEWER','ROLE_ADMIN')")
public @interface IsUser {}

IsEditor.java

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasAnyRole('ROLE_USER','ROLE_EDITOR','ROLE_ADMIN')")
public @interface IsEditor {}

IsReviewer.java

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasAnyRole('ROLE_USER','ROLE_REVIEWER','ROLE_ADMIN')")
public @interface IsReviewer {}

IsAdmin.java

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasAnyRole('ROLE_ADMIN')")
public @interface IsAdmin {}

Spring Security 自带表达式

  • hasRole(),是否领有某一个权限
  • hasAnyRole(),多个权限中有一个即可,如 hasAnyRole("ADMIN","USER")
  • hasAuthority()AuthorityRole 很像,惟一的区别就是 Authority 前缀多了 ROLE_,如 hasAuthority("ROLE_ADMIN") 等价于 hasRole("ADMIN"),能够参考下面 IsUser.java 的写法
  • hasAnyAuthority(),同上,多个权限中有一个即可
  • permitAll(), denyAll(),isAnonymous(), isRememberMe(),通过字面意思能够了解
  • isAuthenticated(), isFullyAuthenticated(),这两个区别就是 isFullyAuthenticated() 对认证的平安要求更高。例如用户通过 记住明码性能 登入到零碎进行敏感操作,isFullyAuthenticated()会返回false,此时咱们能够让用户再输出一次明码以确保安全,而 isAuthenticated() 只有是登入用户均返回true
  • principal(), authentication(),例如咱们想获取登入用户的 id,能够通过principal() 返回的 Object 获取,实际上 principal() 返回的 Object 基本上能够等同咱们本人编写的 CustomUser。而 authentication() 返回的 AuthenticationPrincipal 的父类,相干操作可看 Authentication 的源码。进一步理解能够看前面Controller 编写中获取用户数据的四种办法
  • hasPermission(),参考字面意思即可

如果想进一步理解,能够参考 Intro to Spring Security Expressions。

增加 Thymeleaf 反对

咱们通过 thymeleaf-extras-springsecurity 来增加 Thymeleaf 对 Spring Security 的反对。

Maven 配置

下面的 Maven 配置曾经加过了

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

应用例子

留神咱们要在 html 中增加 xmlns:sec 的反对

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
    <meta charset="UTF-8">
    <title>Admin</title>
</head>
<body>
<p>This is a home page.</p>
<p>Id: <th:block sec:authentication="principal.id"></th:block></p>
<p>Username: <th:block sec:authentication="principal.username"></th:block></p>
<p>Role: <th:block sec:authentication="principal.authorities"></th:block></p>
</body>
</html>

如果想进一步理解请看文档 thymeleaf-extras-springsecurity。

Controller 编写

IndexController.java

本控制器没有任何的权限规定

@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";}
}

UserController.java

在这个控制器中,我综合展现了自定义注解的应用和 4 种获取用户信息的形式

@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";
    }
}

留神

  • 如果有安全控制的办法 A 被同一个类中别的办法调用,那么办法 A 的权限管制会被疏忽,公有办法同样会受到影响
  • Spring 的 SecurityContext 是线程绑定的,如果咱们在以后的线程中新建了别的线程,那么他们的 SecurityContext 是不共享的,进一步理解请看 Spring Security Context Propagation with @Async

Html 的编写

在编写 html 的时候,基本上就是大同小异了,就是留神一点,如果开启了 CSRF,在编写表单 POST 申请的时候增加上暗藏字段,如 <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/> ,不过大家其实不必加也没事,因为 Thymeleaf 主动会加上去的😀。

github 地址及其起源:https://github.com/Smith-Crui…

正文完
 0