Spring boot Spring security,Thymeleaf使用自己的用户类登陆

Spring boot集成Spring security,Thymeleaf,自定义用户登陆验证

  • Spring boot集成Spring security,Thymeleaf,自定义用户登陆验证
      • 引言
      • Spring security
      • 首先,我们得有一个自己的用户类,然后实现UserDetails:
      • Service层
      • Thymeleaf 中的扩展 和 spring security产生的对象
      • 另外关于post请求返回403,被服务器拒绝的情况

引言

这几天忙活着倒腾自己的毕设,是用spring boot开发的,然后就遇到了项目安全性的问题。想着反正自己折腾,就试试没用过的spring security好了,因为不满足于其自身携带的basic验证,所以要重写一些配置和方法。结果发现还有蛮多坑要踩的,这里记录一下,一方面总结一下所学的,然后也很久没写东西了,另一方面希望能帮到一些小伙伴,让小伙伴们少走一点弯路就更好了。

Spring security

spring security 可以很方便的帮我们处理一些安全问题,而且配置灵活。与spring boot和thymeleaf都能很好的互相协作。在spring boot项目的依赖,spring boot想目创建和thymeleaf这里就不细说了,security添加依赖,在pom.xml中添加:

        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-securityartifactId>
        dependency>
        
        <dependency>
            <groupId>org.thymeleaf.extrasgroupId>
            <artifactId>thymeleaf-extras-springsecurity4artifactId>
        dependency>

然后是自定义的配置类,总共有两个:

/**
 * 系统web配置
 * @author krim
 *
 */
@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter {
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/login").setViewName("login");
        registry.addViewController("/").setViewName("login");
    }
}

看方法名字也知道这里是添加了两个controller,将”/login”和”/”的请求映射到了View名为”login”的文件,当然这个”login”就像controller里直接返回的字符串一样,后面还会被视图解析器解析到具体的文件

第二个配置类比较重要:


/**
 * 系统安全配置
 * @author krim
 *
 */
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter  {


    @Autowired  
    SessionRegistry sessionRegistry;  


    @Bean  
    public SessionRegistry getSessionRegistry(){  
        SessionRegistry sessionRegistry = new SessionRegistryImpl();  
        return sessionRegistry;  
    }  


        @Override
        protected void configure(HttpSecurity http) throws Exception {
         http.authorizeRequests()
             //静态资源和一些所有人都能访问的请求
             .antMatchers("/css/**","/staic/**", "/js/**","/images/**").permitAll()
             .antMatchers("/", "/login","/session_expired").permitAll()
             //登录  
             .and().formLogin()
             .loginPage("/login")
             .usernameParameter("userId")       //自己要使用的用户名字段
             .passwordParameter("password")     //密码字段
             .defaultSuccessUrl("/index")     //登陆成功后跳转的请求,要自己写一个controller转发
             .failureUrl("/loginAuthtictionFailed")  //验证失败后跳转的url
             //session管理  
             .and().sessionManagement()
             .maximumSessions(1)                //系统中同一个账号的登陆数量限制
             .sessionRegistry(sessionRegistry)
             .and().and()
             //登出  
             .logout()  
             .invalidateHttpSession(true)  //使session失效  
             .clearAuthentication(true)    //清除证信息
             .and()  
             .httpBasic();
        }

        @Bean
        UserDetailsService sysUserService(){ //注册UserDetailsService 的bean
            return new UserServiceImpl();
        }
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(sysUserService()); //user Details Service验证

        }

}

这个配置类中的配置名字都很直观,也用了注释进行了简单说明,当然能设置的不止这些,具体的大家可以自己试试。

然后就是最最关键的,和UserDetails相关的方法重写,spring security中默认的用户就是这个UserDetails,如果要使用我们自己的用户,则需要改写相关的方法。



emmm,这里第一次编辑时有错误,下面把错误的原因和解决方法记一下,大家最好也注意一下:

    //这是描述错误的例子,大家不要这么写!
    //情景是我们用上面的代码注入了一个UserServiceImpl类的对象
    //如果我们的UserServiceImpl是这样的,那么将会抛出不是单例的错误:
    @Service
    public class UserServiceImpl implements UserService,UserDetailsService{
        //....省略代码
    }
    public interface UserService{ ... }
    //错误原因,我们用@Bean注解后,会按照方法名为key,向spring容器中注入一个对象
    //于是spring容器中将会存在两个UserServiceImpl的实例,他们的名字分别为sysUserService和UserService(另一个是@Service的作用)
    //当使用@Autowired自动装配这个实现类时,会因为这个类不是单例而抛出异常
    //所以正确的做法是
    public class SysUserServiceImpl implements UserDetailsService{ ... }
    @Service
    public class UserServiceImpl implements UserService{ ... }
    //这样分两个service写,问题就解决了

好了言归正传.


首先,我们得有一个自己的用户类,然后实现UserDetails:

public class SysUser implements UserDetails{
    //用户名
    private String userId;
    //密码
    private String password;

    @Transient
    private List roles;

    @Override
    public Collection getAuthorities() {
        return this.roles;
    }

    /**
    * 获取自己定义的用户名
    */
    @Override
    public String getUsername() {
        return this.userId;
    }


    /**
     * 账户是否过期
     */
    @Override
    public boolean isAccountNonExpired() {

        return true;
    }
    /**
     * 账户是否锁定
     */
    @Override
    public boolean isAccountNonLocked() {

        return true;
    }
    /**
     * 验证是否过期
     */
    @Override
    public boolean isCredentialsNonExpired() {

        return true;
    }
    /**
     * 是否禁用
     */
    @Override
    public boolean isEnabled() {

        return true;
    }
    /**
     * 获取自己定义的密码
     */
    @Override
    public String getPassword() {
        return this.password;
    }
    public String getUserId() {
        return userId;
    }
    public void setUserId(String userId) {
        this.userId = userId;
    }
    public void setPassword(String password) {
        this.password = password;
    }

    public List getRoles() {
            return roles;
    }

    public void setRoles(List roles) {
            this.roles = roles;
    }

}

这里有几个返回值一定要设置为true原因如下:

private class DefaultPreAuthenticationChecks implements UserDetailsChecker {
        public void check(UserDetails user) {
            if (!user.isAccountNonLocked()) {
                logger.debug("User account is locked");

                throw new LockedException(messages.getMessage(
                        "AbstractUserDetailsAuthenticationProvider.locked",
                        "User account is locked"));
            }

            if (!user.isEnabled()) {
                logger.debug("User account is disabled");

                throw new DisabledException(messages.getMessage(
                        "AbstractUserDetailsAuthenticationProvider.disabled",
                        "User is disabled"));
            }

            if (!user.isAccountNonExpired()) {
                logger.debug("User account is expired");

                throw new AccountExpiredException(messages.getMessage(
                        "AbstractUserDetailsAuthenticationProvider.expired",
                        "User account has expired"));
            }
        }
    }

这是spring security在进行验证之前进行的检查,另外的方法也是在一些检查的时候会用到。然后需要注意的就是List roles 这个字段,这个字段并不是必须的,只是为了方便getAuthorities方法。在spring security进行验证时,需要产生一个UserDetails,而这个用户的角色信息就保存在Collection里,然后getAuthorities方法就是返回这个用户的权限表

    public final class SimpleGrantedAuthority implements GrantedAuthority {

    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

    private final String role;

    public SimpleGrantedAuthority(String role) {
        Assert.hasText(role, "A granted authority textual representation is required");
        this.role = role;
    }
    // 省略getter setter
    }

这个类只有一个String类型的字段,其保存的就是待会我们要设置的用户角色名,所以定义了一个List,当然,大家也可以用其他的方式来实现这个方法

Service层

/**
 * 自定义user登陆验证服务层
 * @author krim
 *
 */

public class UserServiceImpl implements UserDetailsService{

    @Autowired UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String userId) throws UsernameNotFoundException {
        SysUser user = userMapper.selectByPrimaryKey(userId);
        if (user == null) {
            throw new UsernameNotFoundException("用户名不存在");
        }
        //角色信息
        List roles = roleMapper.getRoleNameBySysUserId(userId);
        List authtictions = new ArrayList<>();
        for (String string : roles) {
            authtictions.add(new SimpleGrantedAuthority(string));
        }
        user.setRoles(authtictions);
        return user;
    }    
}

我这里用的是mybatis,大家可以用自己的实现方式,主要的逻辑就是:

  1. 使用请求中的用户名查出其在持久文件中的数据,并生成自己的用户对象
  2. 获取该用户的角色信息
  3. 将角色信息打包,装到能用getAuthorities方法拿到的地方
  4. 返回这个用户对象

这里会有几个疑问的地方,第一:难道验证不用密码?第二:角色信息是啥;

首先回答第一个问题,这一步的确不验证密码,密码是后面验证的,具体的话是在:
DaoAuthenticationProvider.additionalAuthenticationChecks中,会将刚刚返回的userdetail对象中的密码和请求中的密码进行加密验证。

第二个问题,角色信息,其实说白了就是一些代表角色的字段,spring security在验证的时候会以”ROLE_”开头(即使你的数据库中记录是没有前缀的,也会在验证的时候给你加上,所以还是直接存有前缀的吧)。比如”ROLE_XX”之类的,至于在数据库中怎么保存,那么就仁者见仁,智者见智了,可以在用户表中加个rolesId的字段,然后按照这个字段找角色表,也可一搞个辅助的用户和角色映射的表,这些都没有绝对的要求,只要把对应的角色信息找出来就好了,所以这里不会告诉大家具体的数据库表的设计,因为指不定有人还不用关系型数据库呢,所以我们还是看逻辑上的东西。

然后写个控制层接收验证成功和验证失败的结果处理就好了,其requestmapping就是上面配置类中配置的,这里就不贴出来了。

Thymeleaf 中的扩展 和 spring security产生的对象

在thymeleaf中使用spring security的相关功能,首先,我们需要添加spring security的命名空间,如下:

<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org"
                   xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4">

因为spring security最后会将信息放到session中,所以可以用spring el ${session.SPRING_SECURITY_CONTEXT.authentication.principal}获取对象,这个principal就是我们刚刚返回的userdetials,在服务端也可以用

SecurityContextHolder.getContext().getAuthentication().getPrincipal();
获取对象。

然后在thymeleaf中需要进行权限控制的地方,可以使用sec:authorize="hasRole('ROLE_XXX'),在服务端可以用@Secured(value={"ROLE_XXX","ROLE_YYY"})

另外关于post请求返回403,被服务器拒绝的情况

spring security 为了保护系统的安全,禁止了绝大多数的post请求,用以防止csrf攻击,所以如果要使用post请求的话,还需要加上一些csrf的相关验证,这里给出常用的方法

1.AJAX中的post,在head中添加以下元数据

    <meta name="_csrf" th:content="${_csrf.token }" />
    <meta name="_csrf_header" th:content="${_csrf.headerName}" />

然后设置AJAX的请求头:

    var token = $("meta[name='_csrf']").attr("content");
    var header = $("meta[name='_csrf_header']").attr("content");
    $(document).ajaxSend(function(e,xhr,options){
        xhr.setRequestHeader(header,token);
    });

2.直接提交表单的情况,添加一个隐藏域,随着表单一起提交

    <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">

这里特别感谢dalao们在网上的分享,本文中有很多内容都是学自网上各牛人的博客,然后加入了一些自己的看法,如果有什么不对的地方欢迎指出。同时授之以鱼不如授之以渔,我们在学习的时候一定要思考为什么要这么做,以及实现的技巧逻辑,切不可深陷代码泥潭或者只是照葫芦画瓢当一个代码的搬运工。

你可能感兴趣的:(springboot)