您的位置:首页 > 编程语言 > Java开发

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 security
a383
为了保护系统的安全,禁止了绝大多数的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们在网上的分享,本文中有很多内容都是学自网上各牛人的博客,然后加入了一些自己的看法,如果有什么不对的地方欢迎指出。同时授之以鱼不如授之以渔,我们在学习的时候一定要思考为什么要这么做,以及实现的技巧逻辑,切不可深陷代码泥潭或者只是照葫芦画瓢当一个代码的搬运工。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息