文章目录
- 一、简介:
- 二、简单搭建
- 1. 前期准备
- 2. 通过配置类方式配置 `DelegatingFilterProxy`:
- 3. 配置Security配置类 -> 继承 WebSecurityConfigurerAdapter:
- 三、configure(AuthenticationManagerBuilder auth) 详解
- 1. 用户认证方式
- 2. 权限访问控制的实现
- 四、configure(HttpSecurity http) 详解
- 五、com.fasterxml.jackson.databind.exc.InvalidDefinitionException 解决方案:
一、简介:
Spring Security 是为了基于Spring的应用程序提供的声明式安全保护的安全性框架。Spring Security 提供了完整的安全性解决方案,它能够在Web请求级别和方法调用级别处理身份认证和授权。因为基于Spring框架,所以SPring Security充分使用了一览注入和面向切面技术。
Spring Security 本质上是借助一系列的 Servlet Filter来提供各种安全性功能,但这并不需要我们手动去添加或者创建多个Filter。实际上,我们仅需要配置一个Filter即可。
DelegatingFilterProxy 是一个特殊的Filter,他本身并没有做太多工作,而是将工作委托给了一个注入到Spring应用上下文的Filter实现类。
二、简单搭建
1. 前期准备
- 建立User表
- 搭建其对应的Spring框架,我这随便用了之前的SSM框架。编写对应表的业务代码和页面
2. 通过配置类方式配置 DelegatingFilterProxy
:
Spring Security 本质上是借助一系列的 Servlet Filter来提供各种安全性功能,但这并不需要我们手动去添加或者创建多个Filter。实际上,我们仅需要配置一个Filter即可。
DelegatingFilterProxy 是一个特殊的Filter,他本身并没有做太多工作,而是将工作委托给了一个注入到Spring应用上下文的Filter实现类。
配置类方式配置则需要新建类,并继承 AbstractSecurityWebApplicationInitializer
除此之外,无需其他操作。
(AbstractSecurityWebApplicationInitializer
实现了 WebApplicationInitializer
接口,所以Spring在启动时会发现并调用其onStartup
方法,在 onStartup
方法中注册了 DelegatingFilterProxy
)
public class SpringSecurityInit extends AbstractSecurityWebApplicationInitializer {
}
除此之外
我们可以选择XML方式配置
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
不管是通过XML方式还是配置类方式来配置 DelegatingFilterProxy
。他都会拦截发往应用中的请求,并将请求委托给ID为 springSecurityFilterChain
的bean.
springSecurityFilterChain
本身即是另一个特殊的Filter,他也被称为FilterChainProxy
。他可以链接任意一个或多个其它Filter。Spring Security依赖一些列的 Serlvet Filter 来提供不同的安全特性,但是用户不需要显示声明 springSecurityFilterChain
以及它所链接在一起的其他Filter,当启动Web安全性时,会自动创建这些Filter。
AbstractSecurityWebApplicationInitializer:onStartUp -> insertSpringSecurityFilterChain 中:
3. 配置Security配置类 -> 继承 WebSecurityConfigurerAdapter:
import org.springframework.context.annotation.Configuration;
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.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.Authentication;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @Data: 2019/10/15
* @Des: Spring security 配置类
*/
@Configuration
@EnableWebSecurity // 启用Spring Security
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 配置拦截器保护请求
*
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// 某些路径放行 -- 这里的放行并不是过滤掉Spring Security中的所有filter,,Get请求能ok,但是POST请求还是不能发出去。
// 原因是因为这里的放行并不会绕过csrf防护,默认开启的CSRF的防护会进行拦截,造成403
.antMatchers("/dis/login", "/dis/loginForm", "/dis/loginFail").permitAll()
// 其它请求都需要校验
.anyRequest().authenticated()
.and()
.formLogin()
// 指定自定义登陆页面路径 -- 使用默认登陆页面可以不写
.loginPage("/dis/login")
//指定自定义form表单请求的路径 -- 使用默认登陆页面可以不写
.loginProcessingUrl("/dis/loginForm")
// 指定登陆时用户名的参数名称
.usernameParameter("username")
// 指定登陆时用户密码的参数名称
.passwordParameter("password")
// // 登陆成功后操作
// .successHandler(new AuthenticationSuccessHandler() {
// @Override
// public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
// System.out.println("登陆成功");
// httpServletResponse.setContentType("application/json;charset=utf-8");
// ObjectMapper om = new ObjectMapper();
// PrintWriter out = httpServletResponse.getWriter();
// out.write(om.writeValueAsString("登陆成功"));
// out.flush();
// out.close();
// }
// })
// // 登陆失败后操作
// .failureHandler(new AuthenticationFailureHandler() {
// @Override
// public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
// System.out.println("登陆失败");
// httpServletResponse.setContentType("application/json;charset=utf-8");
// ObjectMapper om = new ObjectMapper();
// PrintWriter out = httpServletResponse.getWriter();
// out.write(om.writeValueAsString("登陆失败"));
// out.flush();
// out.close();
// }
// })
// 登陆成功后访问的路径
.successForwardUrl("/dis/loginSuccess")
// 登陆失败后访问的路径
.failureForwardUrl("/dis/loginFail")
.and()
.logout().logoutUrl("/dis/logout")
.logoutSuccessHandler(new LogoutSuccessHandler() {
@Override
public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
System.out.println("登出失败");
}
})
.and()
.httpBasic()
.and()
// 关闭csrf防护
.csrf().disable();
}
/**
* 配置用户角色内容
*
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 使用内存存储用户。创建了user 和admin两个用户,密码分别是123和123456 ,角色是user和admin
auth.inMemoryAuthentication()
// 需要注意 ,roles方法默认会给角色加上前缀ROLE_,所以这里的.roles("USER")等同于.roles("ROLE_USER"), roles("ADMIN") 等同于 roles("ROLE_ADMIN")
// 选择加密方式后一定在把内置密码加密
.withUser("user").password(new BCryptPasswordEncoder().encode("123")).roles("USER").and()
.withUser("admin").password(new BCryptPasswordEncoder().encode("123456")).roles("ADMIN").and()
.passwordEncoder(new BCryptPasswordEncoder());
}
/**
* 配置Spring Security 的 Filter 链。
*
* @param web
* @throws Exception
*/
@Override
public void configure(WebSecurity web) throws Exception {
// 放行某些路径 - 这里的放行则是完全放行,所以适合放行一些静态资源
web.ignoring().antMatchers("/static/**");
}
}
注:
@EnableWebSecurity 启用了WEB安全功能。Spring Security必须配置在一个实现了 WebSecurityConfigurer 的bean中,Spring提供了其实现类 WebSecurityConfigurerAdapter 继承即可。
WebSecurityConfigurerAdapter 中有三个 configure 方法可以用来配置WEB安全性,具体如下:
- configure(HttpSecurity http) : 配置 如何通过拦截器保护请求
- configure(AuthenticationManagerBuilder auth) : 配置用户服务
- configure(WebSecurity web) : 配置Spring Security 的Filter链
configure(AuthenticationManagerBuilder auth) 配置用户详细信息的方法:
方法名 | 作用 |
accountExpired(boolean accountExpired) | 定义账号是否已经过期 |
accountLocked(boolean accountLocked) | 定义账号是否已经锁定 |
and() | 用来连接配置 |
authorities(GrantedAuthority… authorities)及其重载方法 | 授予用户一项或多项权限 |
credentialsExpired(boolean credentialsExpired) | 定义凭证是否已经过期 |
disabled(boolean disabled) | 定义账号是否被禁用 |
password(String password) | 定义一个用户密码 |
username(String username) | 定义一个用户名 |
roles(String… roles) | 授予某一用户一项或多项角色 |
其实这就可以运行了,效果如下图:
三、configure(AuthenticationManagerBuilder auth) 详解
上面Demo虽然可以使用,但是在实际项目中,用户的认证方式很少会从内存中获取,Spring Security也提供了多种用户认证方式
1. 用户认证方式
- 基于内存的认证 : 上面已经写出,不再赘述。
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 使用内存存储用户
auth.inMemoryAuthentication()
// 选择加密方式后一定在把内置密码加密
.withUser("user").password(new BCryptPasswordEncoder().encode("123")).roles("USER").and()
.withUser("admin").password(new BCryptPasswordEncoder().encode("123456")).roles("ADMIN").and()
.passwordEncoder(new BCryptPasswordEncoder());
}
- 基于数据库表的认证 :
Spring Security 内置了一些sql查询语句,如下图
第一个查询中获取了用户名、密码、是否启用的信息,作为用户认证
第二个查询查找用户所授予的权限,用来鉴权
第三个查询查找用作为群组成员所授予的权限
如果数据库中表的定义和数据填充可以满足上述表,那么就可不用进行操作。但是大部分情况下,我们无法满足上述结构,则就需要自己重写查询语句:
@Autowired
private DataSource dataSource;
/**
* 配置用户角色内容
*
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.jdbcAuthentication()
// 指定数据源,才能连接数据库
.dataSource(dataSource)
// 查询中获取了用户名、密码、是否启用的信息,作为用户认证
// 需要注意,自定义查询语句的时候,返回字段一定要与默认的相同
.usersByUsernameQuery("select `name` username, pwd `password`, true from `user` where `name` = ?")
// 查询查找用户所授予的权限,用来鉴权
.authoritiesByUsernameQuery("select `name` username, role from `user` where `name` = ?")
// 用作为群组成员所授予的权限
// .groupAuthoritiesByUsername()
.passwordEncoder(new BCryptPasswordEncoder());
}
- 自定义用户认证方式:
假设我们需要认证的用户存储在非关系型数据库中,或者我不想通过上述数据库方式认证(别问为什么,就是不想)等情况,我们便可以通过自定义方式配置用户服务。
- 首先需要创建一个实现UserDetails接口的是pojo类,用来保存用户角色。这里面的方法根据实际业务需求进行调整返回参数,这里只是随便写的,真的随便写的
public class MyUserDetials implements UserDetails {
private int id;
private String name;
private String pwd;
private int parentId;
private String role;
private List<GrantedAuthority> grantedAuthorities;
public List<GrantedAuthority> getGrantedAuthorities() {
return grantedAuthorities;
}
public void setGrantedAuthorities(List<GrantedAuthority> grantedAuthorities) {
this.grantedAuthorities = grantedAuthorities;
}
public MyUserDetials() {
}
public MyUserDetials(User user) {
this.id = user.getId();
this.name = user.getName();
this.pwd = user.getPwd();
this.parentId = user.getParentId();
this.role = user.getRole();
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPwd() {
return pwd;
}
public void setPwd(String pwd) {
this.pwd = pwd;
}
public int getParentId() {
return parentId;
}
public void setParentId(int parentId) {
this.parentId = parentId;
}
public String getRole() {
return role;
}
public void setRole(String role) {
this.role = role;
}
// 获取权限集合
@Override
@JsonIgnore
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.grantedAuthorities;
}
@Override
@JsonIgnore
public String getPassword() {
return this.pwd;
}
@Override
public String getUsername() {
return this.name;
}
// 帐户是否没过期
@Override
@JsonIgnore
public boolean isAccountNonExpired() {
return true;
}
// 帐户是否没被冻结
@Override
@JsonIgnore
public boolean isAccountNonLocked() {
return true;
}
// 帐户密码是否没过期,一般有的密码要求性高的系统会使用到,比较每隔一段时间就要求用户重置密码
@Override
@JsonIgnore
public boolean isCredentialsNonExpired() {
return true;
}
// 帐号是否可用
@Override
@JsonIgnore
public boolean isEnabled() {
return true;
}
}
- 创建一个类实现UserDetailsService 接口,并重写其中的loadUserbyUsername方法
@Service
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private UserMapper userMapper;
/**
* 根据userName查询用户
* @param username 用户唯一标识,这里名字是username,并不代表实际就是username
* 实际登陆过程时,肯定是唯一标识+密码进行登录,这里的username代表的是唯一标识,而非字面意义上的用户名
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
MyUserDetials myUserDetials = new MyUserDetials(userMapper.loadUserByUsername(username));
if (myUserDetials == null) {
throw new UsernameNotFoundException("用户名错误");
}
// 创建权限集合
ArrayList<GrantedAuthority> grantedAuthorities = new ArrayList<GrantedAuthority>();
grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_USER"));
myUserDetials.setGrantedAuthorities(grantedAuthorities);
return myUserDetials;
}
}
- 配置使用自定义用户服务
@Autowired
private UserDetailsService userDetailsService;
/**
* 配置用户角色内容
*
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(new BCryptPasswordEncoder());
}
自定义用户验证的方式的好处在于他并不需要知道用户数据存储到什么地方,也不关心地城的数据存储,他只需要获取一个用户对象,即可进行验证。
另外,还可以进一步自定义验证逻辑以满足具体的业务需求:
2. 权限访问控制的实现
这部分写的不够细致,具体细节可以参考(反正我是看他的):
- 自定义FilterInvocationSecurityMetadataSource,主要功能就是通过当前的请求地址,获取该地址需要的用户角色
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.stereotype.Component;
import java.util.Collection;
/**
* @Data: 2019/10/16
* @Des: FilterInvocationSecurityMetadataSource有一个默认的实现类DefaultFilterInvocationSecurityMetadataSource,
* 该类的主要功能就是通过当前的请求地址,获取该地址需要的用户角色
*/
@Component
public class MySecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
@Autowired
private MenuService menuService;
/**
* 查询出具有权限访问当前这个url的角色的集合
* getAttributes(Object o)方法返回的集合最终会来到AccessDecisionManager类中
*
* @param object
*/
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
// 获取请求地址
String requestUrl = ((FilterInvocation) object).getRequestUrl();
// 如果是登录页地址,不需要任何角色即可访问,直接返回null。
if ("dis/login".equals(requestUrl)) {
return null;
}
// 找到所有的菜单页面
List<Menu> allMenu = menuService.getAllMenu();
for (Menu menu : allMenu) {
// 如果和当前访问的url匹配的菜单页面,且这个菜单页面配置了访问角色
if (antPathMatcher.match(menu.getUrl(), requestUrl) && menu.getRoles().size() > 0) {
List<Role> roles = menu.getRoles();
int size = roles.size();
String[] values = new String[size];
for (int i = 0; i < size; i++) {
values[i] = roles.get(i).getName();
}
// 将当前访问的url的可访问角色列表返回
return SecurityConfig.createList(values);
}
}
//没有匹配上的资源,都是登录访问
return SecurityConfig.createList("ROLE_LOGIN");
}
/**
* 获取所有角色
* @return
*/
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
/**
* 判断实现是否可以处理指定的类
*
* @param clazz
* @return
*/
@Override
public boolean supports(Class<?> clazz) {
return FilterInvocation.class.isAssignableFrom(clazz);
}
}
- 自定义AccessDecisionManager,主要功能就是用来判断当前用户是否具备访问当前url的权限
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.Iterator;
/**
* @Data: 2019/10/16
* @Des: 用来判断当前用户是否具备访问当前url的权限
*/
@Component
public class MyAccessDecisionManager implements AccessDecisionManager {
/**
* 用来判断当前用户是否满足调酒啊
*
* @param authentication 保存了当前登录用户的信息,对比当前用户信息,若当前用户不在角色类型中,则抛出异常
* @param object
* @param configAttributes 保存了具备当前页面访问权限的角色,从FilterInvocationSecurityMetadataSource的getAttributes方法获取
* @throws AccessDeniedException
* @throws InsufficientAuthenticationException
*/
@Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
Iterator<ConfigAttribute> iterator = configAttributes.iterator();
while (iterator.hasNext()) {
ConfigAttribute ca = iterator.next();
//当前请求需要的权限
String needRole = ca.getAttribute();
if ("ROLE_LOGIN".equals(needRole)) {
if (authentication instanceof AnonymousAuthenticationToken) {
throw new BadCredentialsException("未登录");
} else
return;
}
//当前用户所具有的权限
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
for (GrantedAuthority authority : authorities) {
if (authority.getAuthority().equals(needRole)) {
return;
}
}
}
throw new AccessDeniedException("权限不足!");
}
@Override
public boolean supports(ConfigAttribute attribute) {
return true;
}
@Override
public boolean supports(Class<?> clazz) {
return true;
}
}
- 自定义AccessDeniedHandler
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
* @Data: 2019/10/16
* @Des: 自定义403情况下的响应内容
*/
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.setCharacterEncoding("UTF-8");
PrintWriter out = response.getWriter();
out.write("{\"status\":\"error\",\"msg\":\"权限不足,请联系管理员!\"}");
out.flush();
out.close();
}
}
- 在configure(HttpSecurity http)中配置。在configure(HttpSecurity http)方法中,通过withObjectPostProcessor将刚刚创建的FilterInvocationSecurityMetadataSource和AccessDecisionManager注入进来。到时候,请求都会经过刚才的过滤器(除了configure(WebSecurity web)方法忽略的请求)。
package com.kingfish.common.config.security;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.ObjectPostProcessor;
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.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.sql.DataSource;
import java.io.IOException;
/**
* @Data: 2019/10/15
* @Des: Spring security 配置类
*/
@Configuration
@EnableWebSecurity // 启用Spring Security
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private MySecurityMetadataSource mySecurityMetadataSource;
@Autowired
private MyAccessDecisionManager myAccessDecisionManager;
@Autowired
private MyAccessDeniedHandler myAccessDeniedHandler;
/**
* 配置拦截器保护请求
*
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// 某些路径放行 -- 这里的放行并不是过滤掉Spring Security中的所有filter,,Get请求能ok,但是POST请求还是不能发出去。
// 原因是因为这里的放行并不会绕过csrf防护,默认开启的CSRF的防护会进行拦截,造成403
.antMatchers("/dis/login", "/dis/loginForm", "/dis/loginFail").permitAll()
// 其它请求都需要校验
.anyRequest().authenticated()
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O o) {
o.setSecurityMetadataSource(mySecurityMetadataSource);
o.setAccessDecisionManager(myAccessDecisionManager);
return o;
}
})
.and()
// 指定自定义登陆页面路径
.formLogin().loginPage("/dis/login")
//指定自定义form表单请求的路径
.loginProcessingUrl("/dis/loginForm")
// 指定登陆时用户名的参数名称
.usernameParameter("username")
// 指定登陆时用户密码的参数名称
.passwordParameter("password")
// // 登陆成功后操作
// .successHandler(new AuthenticationSuccessHandler() {
// @Override
// public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
// System.out.println("登陆成功");
// httpServletResponse.setContentType("application/json;charset=utf-8");
// ObjectMapper om = new ObjectMapper();
// PrintWriter out = httpServletResponse.getWriter();
// out.write(om.writeValueAsString("登陆成功"));
// out.flush();
// out.close();
// }
// })
// // 登陆失败后操作
// .failureHandler(new AuthenticationFailureHandler() {
// @Override
// public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
// System.out.println("登陆失败");
// httpServletResponse.setContentType("application/json;charset=utf-8");
// ObjectMapper om = new ObjectMapper();
// PrintWriter out = httpServletResponse.getWriter();
// out.write(om.writeValueAsString("登陆失败"));
// out.flush();
// out.close();
// }
// })
// 登陆成功后访问的路径 使用successHandler也可
.successForwardUrl("/dis/loginSuccess")
// 登陆失败后访问的路径, 使用failureHandler也可
.failureForwardUrl("/dis/loginFail")
.and()
.logout().logoutUrl("/dis/logout")
.logoutSuccessHandler(new LogoutSuccessHandler() {
@Override
public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
System.out.println("登出失败");
}
})
.and()
.httpBasic()
.and()
// 关闭csrf防护
.csrf().disable()
.exceptionHandling().accessDeniedHandler(myAccessDeniedHandler);
;
}
@Autowired
private DataSource dataSource;
@Autowired
private UserDetailsService userDetailsService;
/**
* 配置用户角色内容
*
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(new BCryptPasswordEncoder());
}
/**
* 配置Spring Security 的 Filter 链。
*
* @param web
* @throws Exception
*/
@Override
public void configure(WebSecurity web) throws Exception {
// 放行某些路径 - 这里的放行则是完全放行,所以适合放行一些静态资源
web.ignoring().antMatchers("/static/**");
}
}
四、configure(HttpSecurity http) 详解
1. 定义如何保护路径的配置方法
方法 | 释义 |
access(String) | 如果给定的SpEL表达式计算结果为true,则允许访问 |
anonyoms() | 允许匿名访问 |
authenticated() | 允许认证过的用户访问 |
denyAll() | 无条件拒绝所有访问 |
fullyAuthenticated() | 如果用户是完整认证的话(不是通过Rememeber-me功能认证的)就允许访问 |
hasAnyAuthority(String… authorities) | 如果用户具备给定权限中的一个的话就允许访问 |
hasAnyRole(String… roles) | 如果用户具备给定角色中的一个的话,就允许访问 |
hasAnyAuthority(String authorities) | 如果用户具备给定权限的话就允许访问 |
hasIpAddress(String ipaddressExpression) | 如果请求来自给定ip地址的话就允许访问 |
hasRole(String) | 如果用户具备给定角色的话,就允许访问 |
not() | 对其他访问方法的结果求反 |
permitAll() | 无条件允许所有访问 |
rememberMe() | 如果用户是通过Remember-me功能认证,就允许访问 |
Demo如下 :
http.authorizeRequests()
// dis/loginSuccess 路径有ROLE_USER角色才可以访问
.antMatchers("dis/loginSuccess").hasAnyAuthority("ROLE_USER")
// dis/login 匿名用户可以访问
.antMatchers("dis/login").anonymous();
2. Spring Security扩展的SpEL表达式
安全表达式 | 计算结果 |
authenticated | 用户认证对象 |
denyAll | 始终未false |
hasAnyRolel(list of roles) | 如果用户被授予了指定的角色之一,则为true |
hasRole(role) | 如果用户被授予了指定的角色,则为true |
hasIpAddress(IP Address) | 如果请求来自指定Ip,则为true |
isAnnoyoms() | 如果当前用户为匿名用户,则为true |
isAuthenticated() | 如果当前用户认证了,则为true |
isFullAuthenticated() | 如果用户是完整认证的话(不是通过Rememeber-me功能认证的)就为true |
isRememberMe() | 如果用户是通过Remember-me功能认证则为true |
permitAll | 结果始终为true |
principal | 用户的principal对象 |
Demo如下 :
http.authorizeRequests()
// dis/loginSuccess 路径 匿名用户且Ip 是本机才可以访问
.antMatchers("dis/loginSuccess").access("isAnonymous() and hasIpAddress('127.0.0.1')");
3. 防止跨站请求伪造 CSRF
CSRF : cross-site request forgery ,即跨站请求伪造。如果一个站点欺骗用户提交请求到其它服务器的话,就会发生CSRF攻击。简单的例子,我登陆一个网站,www.aaa.com 。在这个网站上输入用户名密码,点击登陆。然后这个网站把我的用户名密码提交到的www.bbb.com 这个网站,即造成了跨站请求伪造。
为了解决这个问题,从Spring Security3.2开始,默认就会开启CSRF放防护。Spring Security 通过一个同步token的方式来实现CSRF防护功能。他会拦截状态请求的变化,并检查CSRF token,如果请求汇总不包含 token或token不能与服务期端匹配,则请求失败,抛出CsrfException 异常。
解决方案:
1. 在 “_csrf” 域中提交token
- 通过Thymeleaf模板,在form表单便签action属性添加Thymeleaf命名空间前缀,则会自动生成一个_csrf 隐藏域:
<form method="post" name="login" th:action="@{/dis/loginForm}">
...
</form>
- 使用jsp作为页面模板则如下:
<input type="hidden" name="${_csrf_parameterName}" value="${_csrf.token}">
- 关闭csrf防护
// 关闭csrf防护
.csrf().disable()
4. 启用Remember-me功能
// 后端启用Remember-me功能
.and()
.rememberMe() // 启用remember-me
.tokenValiditySeconds(60) // 过期时间60s
.key("kingfish") //私钥的值, 默认是SpringSecured
// 前端添加复选框
<input type="checkbox" name="remember_me" id="remember_me">
<label for="remember_me" class="inline">Remember me</label>
五、com.fasterxml.jackson.databind.exc.InvalidDefinitionException 解决方案:
第一种: 在 MyUserDetails 类上加上 @JsonIgnoreProperties({“hibernateLazyInitializer”, “handler”})注解
第二种 :使用Fastjson来讲对象转为String
在UserDetials 中使用 @JSONField(serialize=false) 来忽略某些字段的序列化