最近在研究spring security。之前搭建了一个简单的spring security3项目,成功运行后,想试试spring security4.想来应该不用多麻烦,结果在准备不足的情况下,发现security4的框架搭建起来,也没问题,但是就是登陆不上。这个问题困扰了我3天,于是上网查资料,发现4和3还是有很多不一样的地方。在这里将使用security3以及升级到security4的时候遇到的问题记录下来。
1、call refresh...之类的问题
这个是很低级的问题。我出现这种问题有两个原因。一个是schema和xsd没有升级。由于配置是拷贝原来的3的,用正在4里schema自然要换成对应的版本。
还有一个原因是里面有中文注释。去掉中文注释就好。如果一定要加注释,英文好的人用英文表述,英文不要的就用品音吧。
2、MD5盐加密。
密码自然不可能明文存放。MD5+salt现在已经是很多人在用的情况了。但是spring security的UserDetail类不含salt这个属性,解决方法有几种:
1、使用username作为盐。这种方式其实不推荐,因为UserDetail自带的属性不多,而且相对固定的更少。
2、编写org.springframework.security.core.userdetails.User 的子类。我就是采取这种方式,多加了一个salt属性。这样可以在自己的UserDetailService实现类中将salt封装进去并返回。在配置文件中的saltSource中写salt这个属性完全没问题。
3、关于csrf
<input type="text" name="${_csrf.parameterName}" id="" value="${_csrf.token}"/>
这样表单提交之后才能正常验证。不加的话会一直返回403,显示Access Denied。
4、关于AuthenticationSuccessHandler
1)default-target-url和always-use-default-target
<bean id="myAuthenticationSuccessHandler" class="com.yrsoft.security.MyAuthenticationSuccessHandler">
<property name="defaultTargetUrl" value="/hello"></property>
<property name="alwaysUseDefaultTargetUrl" value="true"></property>
</bean>
2)跳转到登陆前的页面
public class MyAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
private Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
private UserService userService;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
SaltedUser saltedUser = (SaltedUser) authentication.getPrincipal();
Long uid = saltedUser.getId();
if(logger.isDebugEnabled()){
logger.debug("Updating User lastLogin");
}
int res = userService.updateLoginDate(uid);
if(res>0 && logger.isDebugEnabled()){
logger.debug("Update Success : " + res);
}
logger.info(request.getRequestURI());
super.onAuthenticationSuccess(request, response, authentication);
}
}
就是先更新了数据库中需要的字段,然后再调用父类的同名方法。
===============2017年2月23日更新============================
5、关于http节点的auto-config和use-expressions
6、关于乱码问题
import javax.servlet.FilterRegistration;
import javax.servlet.ServletContext;
import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer;
import org.springframework.web.filter.CharacterEncodingFilter;
/**
* 主要任务是注册springSecurityFilterChain Filter
*/
public class SecurityWebApplicationInitializer extends AbstractSecurityWebApplicationInitializer {
protected boolean enableHttpSessionEventPublisher() {
return true;
}
/**
* 添加编码过滤
*/
protected void beforeSpringSecurityFilterChain(ServletContext servletContext) {
FilterRegistration.Dynamic characterEncodingFilter = servletContext.addFilter("encodingFilter",
new CharacterEncodingFilter());
characterEncodingFilter.setInitParameter("encoding", "UTF-8");
characterEncodingFilter.setInitParameter("forceEncoding", "true");
characterEncodingFilter.addMappingForUrlPatterns(null, false, "/*");
super.beforeSpringSecurityFilterChain(servletContext);
}
}
其中beforeSpringSecurityFilterChain 的意思是在Security的Filter chain执行前,在这里加了编码Filter之后果然解决问题。
7、关于AccessDecissionManager
/**
* 可在此方法中添加Voter
* @return
*/
@Bean
public AccessDecisionManager accessDecisionManager(){
List<AccessDecisionVoter<? extends Object>> decisionVoters = new ArrayList<>();
decisionVoters.add(roleVoter());
decisionVoters.add(new AuthenticatedVoter());
decisionVoters.add(new WebExpressionVoter());
AbstractAccessDecisionManager accessDecisionManager = new AffirmativeBased(decisionVoters);
accessDecisionManager.setMessageSource(messageSource);
return accessDecisionManager;
}
@Override
@SuppressWarnings("rawtypes")
final List<AccessDecisionVoter<? extends Object>> getDecisionVoters(H http) {
List<AccessDecisionVoter<? extends Object>> decisionVoters = new ArrayList<AccessDecisionVoter<? extends Object>>();
WebExpressionVoter expressionVoter = new WebExpressionVoter();
expressionVoter.setExpressionHandler(getExpressionHandler(http));
decisionVoters.add(expressionVoter);
return decisionVoters;
}
8、关于修改'Role_' 的prefix
<bean id="roleVoter" class=" org.springframework.security.access.vote.RoleVoter">
<property name="rolePrefix" value="unicorn_"></property>
</bean>
ss4的话就简单了,我这使用的java config,看了一下源码里的注释,直接使用hasAuthority方法就行。如:
http.authorizeRequests().antMatchers("/admin").hasAuthority("unicorn_ADMIN");
9、在Thymeleaf中使用SpringSecurity标签
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity4</artifactId>
<version>3.0.2.RELEASE</version>
</dependency>
版本要和当前使用的spring security对应。如果是在使用spring security3,那添加的包就是thymeleaf-extras-springsecurity3.jar
private TemplateEngine templateEngine() {
SpringTemplateEngine engine = new SpringTemplateEngine();
engine.setTemplateResolver(templateResolver());
engine.addDialect(new org.thymeleaf.extras.springsecurity4.dialect.SpringSecurityDialect());
return engine;
}
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4"
>
接下来就能在这个html里使用sec标签了。具体用法自行百度。
10、关于SpringBoot中静态资源的引用
templates目录的配置
static目录的配置
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/");
registry.addResourceHandler("/resource/**").addResourceLocations("classpath:/resource/");
super.addResourceHandlers(registry);
}
否则是引不到的。参考文章:
SpringBoot静态资源处理
这里再次感谢isea533,此大大的博客已经帮我很多次了。
11、添加验证码
继承GenericFilterBean
编写Filter时候的几点考虑
public void doFilter(ServletRequest request,
ServletResponse response,FilterChain chain) throws IOException, ServletException,AuthenticationException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse res = (HttpServletResponse) response;
if (isLoginPage(req)) { //判断是否是登陆的页面
if (postOnly && !req.getMethod().equals("POST")) { //判断是否只能POST请求
throw new AuthenticationServiceException(
"Authentication method not supported: " + req.getMethod());
}
String genCode = this.obtainGeneratedCaptcha(req); //获取生成的验证码
String inputCode = this.obtainCaptcha(req); //获取输入的验证码
if (genCode == null) { //验证失败的情况下通过failureHandler处理请求
failureHandler.onAuthenticationFailure(req, res,
new CaptchaException(this.messages.getMessage("LoginAuthentication.captchaInvalid")));
return;
}
if (!genCode.equalsIgnoreCase(inputCode)) {
failureHandler.onAuthenticationFailure(req, res,
new CaptchaException(this.messages.getMessage("LoginAuthentication.captchaNotEquals")));
return;
}
}
//验证成功的情况下继续后续的Filter
chain.doFilter(request, response);
}
这样去配置一下即可。我是用javaconfig的 http.addFilterAt 的方式替换UsernamePasswordAuthenticationFilter的,已经测试运行成功。
===============2017年3月13日更新============================
12、添加druid监控配置
@Bean
@ConfigurationProperties(prefix="spring.datasource")
public DataSource druidDataSource(){
return new DruidDataSource();
}
/*以下两个bean配置druid监控功能*/
@Bean
public ServletRegistrationBean druidServlet() {
ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(new StatViewServlet(), "/druid/*");
//是否能够重置数据.
servletRegistrationBean.addInitParameter("resetEnable","false");
return servletRegistrationBean;
}
@Bean
public FilterRegistrationBean filterRegistrationBean() {
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
filterRegistrationBean.setFilter(new WebStatFilter());
filterRegistrationBean.addUrlPatterns("/*");
filterRegistrationBean.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*");
return filterRegistrationBean;
}
一般的情况下这样配置就够了。但是在我的项目里,这样配置不生效。情况是:druid的监控界面可以出来,但是页面中没有任何数据。
public CsrfConfigurer<H> requireCsrfProtectionMatcher(
RequestMatcher requireCsrfProtectionMatcher) {
Assert.notNull(requireCsrfProtectionMatcher,
"requireCsrfProtectionMatcher cannot be null");
this.requireCsrfProtectionMatcher = requireCsrfProtectionMatcher;
return this;
}
http.csrf().ignoringAntMatchers("/druid/**")
这个是配置忽略csrf验证的列表。参数类型是一个String类型的可变长参数。
需要注意的是,这里不能配置 /druid** ,一定要/druid/** 这样配置才生效。因为他的RequestMatcher和我的requestMatcher实现方式不一样。
===============2017年3月15日更新============================
13、前端获取CsrfToken
<input type="hidden" id="csrfToken" th:value="${#httpServletRequest.getAttribute('_csrf').token}" />
写了这个之后,就可以在js中通过读取这个id的value来获取csrf的Token了。
14、使用外部容器运行spring boot 项目
1、修改pom文件
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
2、修改代码
让你的主函数所在类继承org.springframework.boot.web.support.SpringBootServletInitializer。@SpringBootApplication
public class UnicornSecurityApplication extends SpringBootServletInitializer{
/**
* 实现SpringBootServletInitializer可以让spring-boot项目在web容器中运行
*/
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
builder.sources(this.getClass());
return super.configure(builder);
}
public static void main(String[] args) throws Exception{
SpringApplication.run(UnicornSecurityApplication.class, args);
}
}
最后,别忘了添加WEB-INF目录以及web.xml。如果使用的Web Module为3.0+,那么可以省略此步骤。因为3.0+可以不用web.xml
15、关于使用spring session redis在第三方容器下的失效问题
The @EnableRedisHttpSession annotation creates a Spring Bean with the name of springSessionRepositoryFilter that implements Filter. The filter is what is in charge of replacing the HttpSession implementation to be backed by Spring Session. In this instance Spring Session is backed by Redis.
Last we need to ensure that our Servlet Container (i.e. Tomcat) uses our springSessionRepositoryFilter for every request. It is extremely important that Spring Session’s springSessionRepositoryFilter is invoked before Spring Security’s springSecurityFilterChain. This ensures that the HttpSession that Spring Security uses is backed by Spring Session. Fortunately, Spring Session provides a utility class named
protected void beforeSpringSecurityFilterChain(ServletContext servletContext) {
FilterRegistration.Dynamic springSessionRepositoryFilter = servletContext.addFilter("springSessionRepositoryFilter",
new DelegatingFilterProxy());
springSessionRepositoryFilter.addMappingForUrlPatterns(null, false, "/*");
super.beforeSpringSecurityFilterChain(servletContext);
}
问题解决。