IT@程序员猿媛SpringBoot精选Java架构技术进阶

SpringBoot Security 整合thymeleaf模

2019-06-19  本文已影响16人  程就人生

使用SpringBoot Security进行登录验证,可以结合具体的业务需求来使用。在
SpringBoot Security前后端分离,登录退出等返回json
一文中,描述了前后端分离的情况下,如何进行登录验证和提示错误信息的。现在针对自定义的登录页面,能够精确地提示错误信息,做一个简单的演示demo。

本文使用的SpringBoot版本是2.1.4.RELEASE,下面直接进入使用阶段。

第一步,在pom.xml中引入架包
<!-- security架包 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

加上这个架包,重启项目后,整个项目就配置了登录拦截和验证。

第二步,重启项目,会在控制台下看到自动生成的登录密码,默认的用户名是admin

图-1

第三步,打开浏览器窗口,对登录页面进行探究

图-2

不输入用户名和密码,直接点击登录时,会有提示信息,输入框的颜色还会变红。查看源码,可以发现,架包默认的登录页面提交方式为表单提交,method为post,并且默认是开启csrf的,在表单里自动生成了一个隐藏域,防止跨域提交,确保请求的安全性。


图-2

输入错误的用户名或密码,可以看到页面进行了跳转,跳转后的页面又回到了登录页,只是url地址后面多了一个参数,页面提示错误信息。


图-3

从页面源码,我们可以获得以下几个方面的信息:

  1. 自带的登录页面使用post方式提交;
  2. 用户名密码错误时,页面会进行重定向,重定向到登录页面,并展示错误信息。

如果页面是我们自己自定义的,如果要使用默认的过滤器获取登录信息,则必须使用post方式进行提交,如果使用ajax json的方式进行提交,则获取不到参数。

接下来自定义一个登录页面,为了快速构建登录页面,这里使用了thymeleaf模板。

第一步,在配置文件中,引入thymeleaf架包
<!-- 导入Spring Boot的thymeleaf依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
第二步,在resouce的templates下建立登录页面login.html
<!DOCTYPE HTML>
<!-- thymeleaf模板必须引入 -->
<!DOCTYPE HTML>
<!-- thymeleaf模板必须引入 -->
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>SpringBoot模版渲染</title>
    <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/>
</head>
<body>
<form th:action="@{/login}" method="post" >
    <input th:text="用户名" type="text" name="username" />
    <input th:text="密码" type="password" name="password" />
    <button type="submit" >提交</button>
    <!-- ${session?.SPRING_SECURITY_LAST_EXCEPTION?.message} security自带的错误提示信息 -->
    <p th:if="${param.error}" th:text="${session?.SPRING_SECURITY_LAST_EXCEPTION?.message}" ></p>
</form>
</body>
</html>
第三步,添加security的配置文件,为了debug登录情况,对UserDetails、MyPasswordEncoder、UserDetailsService三个接口进行了实现,并在配置文件中进行了配置
package com.example.demo.security;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.csrf.CsrfFilter;
import org.springframework.web.filter.CharacterEncodingFilter;

/**
 * SpringSecurity的配置
 * @author 程就人生
 * @date 2019年5月26日
 */
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Autowired
    private UserDetailsService myCustomUserService;

    @Autowired
    private MyPasswordEncoder myPasswordEncoder;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        
        http.cors().and().csrf().disable();
        
        http
            //使用form表单post方式进行登录
            .formLogin()
            //登录页面为自定义的登录页面
            .loginPage("/login")
            //设置登录成功跳转页面,error=true控制页面错误信息的展示
            .successForwardUrl("/index").failureUrl("/login?error=true")
            .permitAll()
            .and()
            //允许不登陆就可以访问的方法,多个用逗号分隔
            .authorizeRequests().antMatchers("/test").permitAll()
            //其他的需要授权后访问
            .anyRequest().authenticated();        
        
            //session管理,失效后跳转  
            http.sessionManagement().invalidSessionUrl("/login"); 
            //单用户登录,如果有一个登录了,同一个用户在其他地方登录将前一个剔除下线 
            //http.sessionManagement().maximumSessions(1).expiredSessionStrategy(expiredSessionStrategy()); 
            //单用户登录,如果有一个登录了,同一个用户在其他地方不能登录 
            http.sessionManagement().maximumSessions(1).maxSessionsPreventsLogin(true); 
            //退出时情况cookies
            http.logout().deleteCookies("JESSIONID"); 
            //解决中文乱码问题 
            CharacterEncodingFilter filter = new CharacterEncodingFilter(); 
            filter.setEncoding("UTF-8"); filter.setForceEncoding(true); 
            //
            http.addFilterBefore(filter,CsrfFilter.class); 
    }
    
//  @Bean
//  public SessionInformationExpiredStrategy expiredSessionStrategy() {
//      return new ExpiredSessionStrategy();
//  }
    
    @Bean
    public DaoAuthenticationProvider daoAuthenticationProvider() {
        DaoAuthenticationProvider bean = new DaoAuthenticationProvider();
        //返回错误信息提示,而不是Bad Credential
        bean.setHideUserNotFoundExceptions(true);
        //覆盖UserDetailsService类
        bean.setUserDetailsService(myCustomUserService);
        //覆盖默认的密码验证类
        bean.setPasswordEncoder(myPasswordEncoder); 
        return bean;
    }

    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(this.daoAuthenticationProvider());
    }
}

在这个配置中,对登录页面进行了设置,设置使用自定义的登录页面,在Controller需要添加对应的页面渲染。

package com.example.demo.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;
/**
 * 
 * @author 程就人生
 * @date 2019年6月8日
 */
@RestController
public class IndexController {
    
    @RequestMapping("/index")
    public ModelAndView index(){
        return new ModelAndView("/index");
    }
    
    @RequestMapping("/test")
    public Object test(){
        return "test";
    }
    
    /**
     * 自定义登录页面
     * @param error 错误信息显示标识
     * @return
     *
     */
    @GetMapping("/login")
    public ModelAndView login(String error){
         ModelAndView modelAndView = new ModelAndView("/login");
         modelAndView.addObject("error", error);
        return modelAndView;
    }   
}
package com.example.demo.security;

import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

/**
 * 登录专用类,用户登陆时,通过这里查询数据库
 * 自定义类,实现了UserDetailsService接口,用户登录时调用的第一类
 * @author 程就人生
 * @date 2019年5月26日
 */
@Component
public class MyCustomUserService implements UserDetailsService {

    /**
     * 登陆验证时,通过username获取用户的所有权限信息
     * 并返回UserDetails放到spring的全局缓存SecurityContextHolder中,以供授权器使用
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //在这里可以自己调用数据库,对username进行查询,看看在数据库中是否存在
        MyUserDetails myUserDetail = new MyUserDetails();
        if(StringUtils.isEmpty(username)){
            throw new RuntimeException("用户名不能为空!");
        }
        if(!username.equals("admin")){
            throw new RuntimeException("用户名不存在!");
        }
        myUserDetail.setUsername(username);
        myUserDetail.setPassword("123456");
        return myUserDetail;
    }
}
package com.example.demo.security;

import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;

/**
 * 自定义的密码加密方法,实现了PasswordEncoder接口
 * @author 程就人生
 * @date 2019年5月26日
 */
@Component
public class MyPasswordEncoder implements PasswordEncoder {

    @Override
    public String encode(CharSequence charSequence) {       
        //加密方法可以根据自己的需要修改
        return charSequence.toString();
    }

    @Override
    public boolean matches(CharSequence charSequence, String s) {
        return encode(charSequence).equals(s);
    }
}
package com.example.demo.security;

import java.util.Collection;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

/**
 * 实现了UserDetails接口,只留必需的属性,也可添加自己需要的属性
 * @author 程就人生
 * @date 2019年5月26日
 */
public class MyUserDetails implements UserDetails {

    private static final long serialVersionUID = 1L;

    //登录用户名
    private String username;
    //登录密码
    private String password;

    private Collection<? extends GrantedAuthority> authorities;

    public void setUsername(String username) {
        this.username = username;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public void setAuthorities(Collection<? extends GrantedAuthority> authorities) {
        this.authorities = authorities;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return this.authorities;
    }

    @Override
    public String getPassword() {
        return this.password;
    }

    @Override
    public String getUsername() {
        return this.username;
    }

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

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

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

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

第四步,重新启动项目,看一下登录页面的效果
图-4

一个很丑的登录页面,这不是重点。重点是,登录名和密码正确时,页面可以正确的跳转,输入错误时,可以在登录页面进行信息提示。

在MyCustomUserService类中,我们设置了用户名为admin,密码为123456;输入其他的用户名称时,提示用户不存在;不输入用户名称,提示用户不能为空;密码不是123456时,提示密码错误;输入admin,123456时,页面前往index页面,下面进行验证。

第一步,测试不输入用户名,测试结果ok
图-5
第二步,测试输入错误的用户名,测试结果ok
图-6
第三步,测试输入正确的用户名和错误的密码,测试结果ok
图-7
第四步,测试输入正确的用户名和密码,测试结果ok
图-8
第五步,在index.html页面中加个退出按钮,测试一下退出操作,这里为了简单,写了一个表单
<!DOCTYPE HTML>
<!-- thymeleaf模板必须引入 -->
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>SpringBoot模版渲染</title>
    <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/>
</head>
<body>
index
<form th:action="@{/logout}" method="get" >
<button type="submit" >退出</button>
</form>
</body>
</html>
第六步,不登录,访问index时,是直接跳往登录页面的,登录后直接跳往index页面,测试结果ok
图-9
图-10 有帮助,微信扫一扫,关注一下
上一篇下一篇

猜你喜欢

热点阅读