这几天忙活着倒腾自己的毕设,是用spring boot开发的,然后就遇到了项目安全性的问题。想着反正自己折腾,就试试没用过的spring security好了,因为不满足于其自身携带的basic验证,所以要重写一些配置和方法。结果发现还有蛮多坑要踩的,这里记录一下,一方面总结一下所学的,然后也很久没写东西了,另一方面希望能帮到一些小伙伴,让小伙伴们少走一点弯路就更好了。
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,如果要使用我们自己的用户,则需要改写相关的方法。
//这是描述错误的例子,大家不要这么写!
//情景是我们用上面的代码注入了一个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写,问题就解决了
好了言归正传.
public class SysUser implements UserDetails{
//用户名
private String userId;
//密码
private String password;
@Transient
private List 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 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 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
,当然,大家也可以用其他的方式来实现这个方法
/**
* 自定义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,大家可以用自己的实现方式,主要的逻辑就是:
getAuthorities
方法拿到的地方这里会有几个疑问的地方,第一:难道验证不用密码?第二:角色信息是啥;
首先回答第一个问题,这一步的确不验证密码,密码是后面验证的,具体的话是在:
DaoAuthenticationProvider.additionalAuthenticationChecks中
,会将刚刚返回的userdetail对象中的密码和请求中的密码进行加密验证。
第二个问题,角色信息,其实说白了就是一些代表角色的字段,spring security在验证的时候会以”ROLE_”开头(即使你的数据库中记录是没有前缀的,也会在验证的时候给你加上,所以还是直接存有前缀的吧)。比如”ROLE_XX”之类的,至于在数据库中怎么保存,那么就仁者见仁,智者见智了,可以在用户表中加个rolesId的字段,然后按照这个字段找角色表,也可一搞个辅助的用户和角色映射的表,这些都没有绝对的要求,只要把对应的角色信息找出来就好了,所以这里不会告诉大家具体的数据库表的设计,因为指不定有人还不用关系型数据库呢,所以我们还是看逻辑上的东西。
然后写个控制层接收验证成功和验证失败的结果处理就好了,其requestmapping就是上面配置类中配置的,这里就不贴出来了。
在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"})
。
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们在网上的分享,本文中有很多内容都是学自网上各牛人的博客,然后加入了一些自己的看法,如果有什么不对的地方欢迎指出。同时授之以鱼不如授之以渔,我们在学习的时候一定要思考为什么要这么做,以及实现的技巧逻辑,切不可深陷代码泥潭或者只是照葫芦画瓢当一个代码的搬运工。