thymeleaf extras security 不适用于 spring Security

Posted

技术标签:

【中文标题】thymeleaf extras security 不适用于 spring Security【英文标题】:thymeleaf extras security doesn't work with spring Security 【发布时间】:2021-03-25 13:12:30 【问题描述】:

所以作为初学者,我尝试使用 spring boot 2.2.11、spring security、thymeleaf 和 json web token 创建一个电子商务网站,我的问题是当用户验证模板时,即使我输入 isAnonyms 和IsAuthentificated 我的模板中的百里香标签。

我有两个问题:

1-/ 如何告诉所有控制器用户已经登录?

2-/ 如何将 jwt 令牌从后端传递到前端,以便用户可以提出特定的请求?

这是我的 pom.xml:


<?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.2.11.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>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-data-jpa</artifactId>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.webjars/bootstrap -->
        <dependency>
            <groupId>org.webjars</groupId>
            <artifactId>webjars-locator</artifactId>
            <version>0.30</version>
        </dependency>
        <dependency>
            <groupId>org.webjars</groupId>
            <artifactId>bootstrap</artifactId>
            <version>4.0.0</version>
        </dependency>
        <dependency>
            <groupId>org.thymeleaf.extras</groupId>
            <artifactId>thymeleaf-extras-springsecurity5</artifactId>

        </dependency>

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


        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>
        <dependency>
            <groupId>com.example</groupId>
            <artifactId>demo</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>org.thymeleaf</groupId>
            <artifactId>thymeleaf</artifactId>
            <version>3.0.11.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.thymeleaf.extras</groupId>
            <artifactId>thymeleaf-extras-springsecurity5</artifactId>
            <version>3.0.4.RELEASE</version>
        </dependency>
    </dependencies>

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

</project>

我的 Index.html 中包含 thymeleaf 标签的部分:



                <div class="forms ml-auto">
                    <a th:href="@/login" class="btn" sec:authorize="isAnonymous()"><span
                            class="fa fa-user-circle-o"></span> Sign In</a>
                    <a th:href="@/signup" class="btn" sec:authorize="isAnonymous()"><span
                            class="fa fa-pencil-square-o"></span> Sign Up</a>
                    <a th:href="@/account" class="btn" sec:authorize="isAuthenticated()"><span
                            class="fa fa-pencil-square-o"></span> Account</a>
                    <a th:href="@/cart" class="btn"> Cart <span> 0 </span> <i class="fa fa-shopping-cart"></i> </a>
                    <a th:href="@/logout" class="btn" sec:authorize="isAuthenticated()"><span
                            class="fa fa-user-circle-o"></span> Logout</a>

                </div>

我的登录控制器:


 @RequestMapping(value = "/login", method = RequestMethod.GET)
    public String login(Model model) 
        model.addAttribute("userDto",new UserDto());
        return "signin";
    
    @RequestMapping(value = "/login",method = RequestMethod.POST)
    public String login(@ModelAttribute("userDto") @Valid UserDto userDto, BindingResult result , RedirectAttributes ra)
        authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(userDto.getEmail(),userDto.getPassword()));
        final UserDetails userDetails = userDetailsService.loadUserByUsername(userDto.getEmail());
        if (!userDetails.getUsername().equalsIgnoreCase(userDto.getEmail()) )

            result.rejectValue("email",null,"Wrong Email");
        

        if (!bCryptPasswordEncoder.matches(userDto.getPassword(),userDetails.getPassword()))
            result.rejectValue("password","null","Wrong Password");
        
        if (result.hasErrors())
            ra.addFlashAttribute("userDto",userDto);
            return "signin";
        
        final String jwt = jwtUtil.generateToken(userDetails);
        System.out.println(jwt);
        return "index";

    

我的 Spring 安全配置:


    @Override
    protected void configure(HttpSecurity http) throws Exception 
        http.authorizeRequests().antMatchers("/resources/**", "/static/**", "/public/**").permitAll()
                .antMatchers("/", "/signin/", "/signup","/**").permitAll()
                .antMatchers("/admin/**").hasRole("ADMIN")
                .antMatchers("/user/**")
                .hasAnyRole("USER", "ADMIN")

                .anyRequest().authenticated().and().sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                    .and().formLogin().loginPage("/signin").defaultSuccessUrl("/")
                    .usernameParameter("email").passwordParameter("password")
                    .permitAll()
                    .defaultSuccessUrl("/",true)
                .and().logout().logoutSuccessUrl("/")
                .logoutRequestMatcher(new AntPathRequestMatcher("/home/logout"));


        http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);


    

登录前:

img_before_login

登录后的图片:

redirect to index

登录成功并切换到其他页面:

switch page

ps:我会感谢任何解决方案或任何建议。

【问题讨论】:

您的登录有缺陷。这应该是弹簧安全过滤器链的一部分而不是单独的控制器。您没有(正确地)与 Spring Security 交互。所以删除它并让 Spring Security 处理登录(它已经应该通过您配置的 formLogin 来完成。 【参考方案1】:

您可以通过在@Controller 中将Principal 指定为method argument 来获取用户是否通过身份验证。如果值为null,则请求未通过身份验证。否则,请求通过身份验证。

@GetMapping("/foo")
String foo(Principal principal) 
    boolean isAuthenticated = principal != null;
    ...

当认证成功时,您通常会提供 JWT。这是example application。

第一步是提供一种对用户进行身份验证的方法。在本例中,我们使用基本身份验证验证用户名/密码。

@Configuration
public class RestConfig extends WebSecurityConfigurerAdapter 

    @Value("$jwt.public.key")
    RSAPublicKey key;

    @Override
    protected void configure(HttpSecurity http) throws Exception 
        // @formatter:off
        http.authorizeRequests((authz) -> authz.anyRequest().authenticated())
            .csrf((csrf) -> csrf.ignoringAntMatchers("/token"))
            .httpBasic(Customizer.withDefaults())
            .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
            .sessionManagement((session) -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .exceptionHandling((exceptions) -> exceptions
                .authenticationEntryPoint(new BearerTokenAuthenticationEntryPoint())
                .accessDeniedHandler(new BearerTokenAccessDeniedHandler())
            );
        // @formatter:on
    

    @Bean
    UserDetailsService users() 
        // @formatter:off
        return new InMemoryUserDetailsManager(
            User.withUsername("user")
                .password("nooppassword")
                .authorities("app")
                .build()
        );
        // @formatter:on
    

    @Bean
    JwtDecoder jwtDecoder() 
        return NimbusJwtDecoder.withPublicKey(this.key).build();
    


然后在基本身份验证成功后,它到达在响应中产生成功 JWT 的控制器:

@RestController
public class TokenController 

    @Value("$jwt.private.key")
    RSAPrivateKey key;

    @PostMapping("/token")
    public String token(Authentication authentication) 
        Instant now = Instant.now();
        long expiry = 36000L;
        // @formatter:off
        String scope = authentication.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority)
                .collect(Collectors.joining(" "));
        JWTClaimsSet claims = new JWTClaimsSet.Builder()
                .issuer("self")
                .issueTime(new Date(now.toEpochMilli()))
                .expirationTime(new Date(now.plusSeconds(expiry).toEpochMilli()))
                .subject(authentication.getName())
                .claim("scope", scope)
                .build();
        // @formatter:on
        JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.RS256).build();
        SignedJWT jwt = new SignedJWT(header, claims);
        return sign(jwt).serialize();
    

    SignedJWT sign(SignedJWT jwt) 
        try 
            jwt.sign(new RSASSASigner(this.key));
            return jwt;
        
        catch (Exception ex) 
            throw new IllegalArgumentException(ex);
        
    


注意:您没有特别询问,但 Thymeleaf 标记似乎不起作用的可能原因是您处于无状态应用程序中,因此登录后身份验证会立即丢失,因为未创建会话.

【讨论】:

感谢 Rob 的回复,我理解您回复的第一部分。对于第二部分,我有一个类名 jwt,其中包含密钥和所有其他方法,如提取声明、令牌过期、创建令牌。我也有一个类 jwtFilter ,它包含方法 doFilterInternal 的覆盖。所以我的问题是有必要创建类 RestConfig 还是只为令牌创建控制器(如果是的话,我应该在我的 Spring 安全配置类中添加什么?)

以上是关于thymeleaf extras security 不适用于 spring Security的主要内容,如果未能解决你的问题,请参考以下文章

thymeleaf-extras-db 0.0.1发布,select标签加载数据的新姿势

如何在没有 xml 的情况下配置 thymeleaf-extras-springsecurity4?

SpringBoot + Thymeleaf + Security Dialect 怎么配置?

Thymeleaf模板格式化LocalDatetime时间格式

CSS 样式表不适用于 Spring Security + Spring Boot + Thymeleaf

使用 Spring Security 的 Thymeleaf 授权不起作用[重复]