Spring boot Spring security,Thymeleaf使用自己的用户类登陆
2018-03-03 22:50
489 查看
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中添加:<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-security --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!-- 在thymeleaf中扩展spring secutity --> <dependency> <groupId>org.thymeleaf.extras</groupId> <artifactId>thymeleaf-extras-springsecurity4</artifactId> </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< 4000 /span> 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<SimpleGrantedAuthority> roles; @Override public Collection<? extends GrantedAuthority> 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<SimpleGrantedAuthority> getRoles() { return roles; } public void setRoles(List<SimpleGrantedAuthority> 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<? extends GrantedAuthority>里,然后
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<String> roles = roleMapper.getRoleNameBySysUserId(userId); List<SimpleGrantedAuthority> authtictions = new ArrayList<>(); for (String string : roles) { authtictions.add(new SimpleGrantedAuthority(string)); } user.setRoles(authtictions); return user; } }
我这里用的是mybatis,大家可以用自己的实现方式,主要的逻辑就是:
使用请求中的用户名查出其在持久文件中的数据,并生成自己的用户对象
获取该用户的角色信息
将角色信息打包,装到能用
getAuthorities方法拿到的地方
返回这个用户对象
这里会有几个疑问的地方,第一:难道验证不用密码?第二:角色信息是啥;
首先回答第一个问题,这一步的确不验证密码,密码是后面验证的,具体的话是在:
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 securitya383
为了保护系统的安全,禁止了绝大多数的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结合使用拦截器(判断是否用户是否已登陆)
- 深入学习spring-boot系列(三)--使用thymeleaf模板
- 深入学习spring-boot系列(三)--使用thymeleaf模板
- 深入学习spring-boot系列(三)--使用thymeleaf模板
- 深入学习spring-boot系列(三)--使用thymeleaf模板
- 深入学习spring-boot系列(三)--使用thymeleaf模板
- springboot+springSecurity+springSessionDataRedis+CAS搭建集群单点登陆系统
- 深入学习spring-boot系列(三)--使用thymeleaf模板
- Spring Boot—— Thymeleaf (gradle) 的简单使用
- 深入学习spring-boot系列(三)--使用thymeleaf模板
- springboot使用webmagic框架来抓取自己的博客信息
- (18)使用模板(thymeleaf-freemarker)【从零开始学Spring Boot】
- 深入学习spring-boot系列(三)--使用thymeleaf模板
- 深入学习spring-boot系列(三)--使用thymeleaf模板
- spring boot(四):thymeleaf使用详解
- 深入学习spring-boot系列(三)--使用thymeleaf模板
- 深入学习spring-boot系列(三)--使用thymeleaf模板
- 深入学习spring-boot系列(三)--使用thymeleaf模板
- 深入学习spring-boot系列(三)--使用thymeleaf模板
- 深入学习spring-boot系列(三)--使用thymeleaf模板