注:之前写过一些列的SpringSecurity的文章,重新写一遍是为了把某些不必要的步骤省去,留下精简的,因为工作中有一些不需要。
在java的权限框架里,shiro和SpringSecurity是用的最多的。随着springboot的流行,SpringSecurity也越来越火了,因为springboot默认支持SpringSecurity。所以很有必要把SpringSecurity也搞明白。shiro更加轻量级,SpringSecurity的功能更加丰富。
软件环境:
开发工具:Idea
JDK:1.8
maven:3.3.9
* SpringBoot版本:1.5.18 (说明:springboot版本没有用最新版的2.0x,因为2.0x和1.5 差别比较大吧,2.x好似是基于spring5的,SpringSecurity的更新速度好似跟不上springboot,所以这里如果用springboot 2.x会有问题,保险起见这里就不求新了)
SpringSecurity核心功能:认证、授权、攻击防护(防止伪造身份)
话不多说,下边开始打代码。
一、项目搭建
1.1 热身 ,SpringSecurity默认的Httpbasic鉴权
1.1.1,新建项目
填好项目信息,next
选启动器:SpringSecurity、Web
设置项目目录。finish
建好后的项目结构:security包放有关SpringSecurity的代码
maven依赖:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <scope>test</scope> </dependency> </dependencies>
引入了SpringSecurity的starter后,会自动引入SpringSecurity相关的相关依赖:
1.1.2 访问我们的服务
新建一个Controller:
@RestController public class UserController { @RequestMapping("/hello") public String hello(){ return "Hello ! "; } }
启动项目,访问localhost:8080/hello,默认就会出来这个登录框,只要引入了SpringSecurity,springboot就默认会启用http basic认证,所有的服务都会被保护起来, ,。
其中的默认用户名是user,默认的密码在启动项目的时候会在控制台打印。
输入用户名密码,就能访问:
如果输入的用户名或密码错误,Springboot会引导你到一个空白页,这个也是可以配置的,后边再说怎么配置:
你还可以关闭这个默认的鉴权,只需在application.properties 里配置: security.basic.enabled = false ,这个值默认是true。
二、表单登录
如果想去掉那个比较丑的basic登录框,只需要一个配置类即可。新建配置类 BrowserSecurityConfig,继承 WebSecurityConfigurerAdapter
@EnableWebSecurity public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter { }
(为什么这么写?因为SpringSecurity官方文档这么说的https://docs.spring.io/spring-security/site/docs/4.2.10.RELEASE/reference/htmlsingle/#samples):
5.1 Hello Web Security Java配置
第一步是创建Spring Security Java配置。配置创建一个Servlet过滤器,称为springSecurityFilterChain
负责应用程序内所有安全性(保护应用程序URL,验证提交的用户名和密码,重定向到登录表单等)。您可以在下面找到Spring Security Java配置的最基本示例:
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.*; import org.springframework.security.config.annotation.authentication.builders.*; import org.springframework.security.config.annotation.web.configuration.*; @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Bean public UserDetailsService userDetailsService() throws Exception { InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager(); manager.createUser(User.withUsername("user").password("password").roles("USER").build()); return manager; } }
然后启动application,访问http://localhost:8080/hello,可以看到已经出来了一个登录页面,输入用户名user,以及控制台打印的密码:
登录后 访问的是登录之前的要访问的hello服务:
官网说明:
到目前为止,我们的WebSecurityConfig仅包含有关如何验证用户身份的信息。Spring Security如何知道我们要求所有用户进行身份验证?Spring Security如何知道我们想要支持基于表单的身份验证?原因是WebSecurityConfigurerAdapter
在configure(HttpSecurity http)
方法中提供了一个默认配置,如下所示:
protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .anyRequest().authenticated() .and() .formLogin() .and() .httpBasic(); }
三、自定义用户认证逻辑
3.1 ,处理用户信息获取 、用户校验 、密码的加密解密
新建一个类,MyUserDetailService (这个类必须是Spring里的一个Bean,所以加上@Component注解)实现 UserDetailsService 接口,UserDetailsService 接口只有一个方法:通过用户名查询用户信息,返回UserDetail
UserDetailsService.java
public interface UserDetailsService { UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException; }
UserDetails.java : 提供了几个方法,账户是否启用、账户是否过期、密码是否过期、账户是否锁定、权限集合信息
public interface UserDetails extends Serializable { Collection<? extends GrantedAuthority> getAuthorities(); String getPassword(); String getUsername(); boolean isAccountNonExpired(); boolean isAccountNonLocked(); boolean isCredentialsNonExpired(); boolean isEnabled(); }
配置一个密码的加密解密器类:PasswordEncoder,我们用其一个实现类 BCryptPasswordEncoder,只有两个方法,一个是加密密码,一个是匹配方法。如果已有系统已经有了自己的加密算法,这里可以换成自己的加解密逻辑。
@Bean public PasswordEncoder passwordencoder(){ //BCryptPasswordEncoder implements PasswordEncoder return new BCryptPasswordEncoder(); }
这个接口的实现类会给加密的密码随机加盐,所以一样的密码每次加密出来是不一样的,更安全。如123456加密2次:
加密后密码: $2a$10$BChH.C4.X8MYuI1mHFoOkefWhOsad7SvhZedHFt1OG4vjSu.z9weC
加密后密码: $2a$10$YUbz.miE5C0aAcuU1FnHSu/U.Qm/BujTNw6X7S5i4/6AhjyDc6suK
此时我们的逻辑是,只要密码输入123456,就能登录成功。启动项目试验:访问/hello ,会自动跳转/login ,随便输入用户名aaaaaa,输入密码123456 ,成功访问Hello!
|
3.2 自定义登录页面
用SpringSecurity提供的默认的登录页肯定是不行的,所以需要自定义登录页(当然这里还可以配置成一个Controller方法,然后跳转到登录页。或者从配置文件里读取),在BrowserSecurityConfig类里配置:
@Override protected void configure(HttpSecurity http) throws Exception { //http.httpBasic() //这个就是默认的弹框认证 http.formLogin() //表单认证 .loginPage("/login.html") .loginProcessingUrl("/authentication/form") .and() .authorizeRequests() //下边的都是授权的配置 .antMatchers("/login.html").permitAll() //放过登录页不过滤 .anyRequest() //任何请求 .authenticated(); //都需要身份认证 }
.loginPage("/login.html") 配置登录页面
.loginProcessingUrl("/authentication/form") 配置处理登录表单的action,这个值默认在UsernamePasswordAuthenticationFilter 类里,
注意一定要放过登录页不过滤,否则一直不能跳转到login.html
此时访问 /hello ,跳转到自定义的登录页:
随便输入用户名,密码输入123456 ,登录,出现
SpringSecurity默认提供了CSRF(跨站请求伪造)防护,是用CSRF token来完成防护的。暂时关闭CSRF防护,在BrowserSecurityConfig里设置:
.csrf().disable();
再次登录,可以成功访问到 /hello服务。
至此自定义登录页面完成吗,下一步自定义登录成功处理。
也可以把登录页做成在配置文件application.properties里配置,然后在代码里读取配置,这样更灵活、更通用。
这里有springboot读取配置文件的用法:https://www.cnblogs.com/lihaoyang/p/10223339.html
3.2 自定义登录成功/失败处理
现在的流程是,访问一个需要登录的服务,如果没有登录,跳转登录页,等你登录后,立马就访问登录之前的服务。如果你想在登录成功后做一些处理,比如签到,送积分等等。那就需要自定义登录成功的处理。如果没有这种需求,这一步骤可以略过。
springsecurity提供了一个接口,AuthenticationSuccessHandler,用来处理登录成功后的逻辑:
/**
* Strategy used to handle a successful user authentication.
* <p>
* Implementations can do whatever they want but typical behaviour would be to control the
* navigation to the subsequent destination (using a redirect or a forward). For example,
* after a user has logged in by submitting a login form, the application needs to decide
* where they should be redirected to afterwards (see
* {@link AbstractAuthenticationProcessingFilter} and subclasses). Other logic may also be
* included if required.
*
* @author Luke Taylor
* @since 3.0
*/
public interface AuthenticationSuccessHandler {
/**
* Called when a user has been successfully authenticated.
*
* @param request the request which caused the successful authentication
* @param response the response
* @param authentication the <tt>Authentication</tt> object which was created during
* the authentication process.
*/
void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response, Authentication authentication)
throws IOException, ServletException;
}
1,定义一个自己的成功处理器,实现这个Handler:
package com.lhy.browser.security.authentication; import com.fasterxml.jackson.databind.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.stereotype.Component; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * 认证成功处理器 */ @Component("myAuthenticationSuccessHandler") public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler { private Logger logger = LoggerFactory.getLogger(getClass()); //springmvc启动会自动注册一个ObjectMapper @Autowired private ObjectMapper objectMapper; @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { logger.info("登录成功"); //把authentication返回给响应 response.setContentType("application/json;charset=UTF-8"); response.getWriter().write(objectMapper.writeValueAsString(authentication)); } }
2、添加到配置类,让spring security执行自定义的处理器
在 BrowserSecurityConfig 配置类里注入 AuthenticationSuccessHandler
@EnableWebSecurity
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
//自定义的认证成功的处理器
@Autowired
private AuthenticationSuccessHandler myAuthenticationSuccessHandler;
//自定义的认证失败的处理器
@Autowired
private AuthenticationFailureHandler myAuthenticationFailureHandler;
@Autowired
private SecurityProperties securityProperties;
@Override
protected void configure(HttpSecurity http) throws Exception {
//http.httpBasic() //这个就是默认的弹框认证
http.formLogin() //表单认证
.loginPage(securityProperties.getBrowser().getLoginPage())//登录页
.loginProcessingUrl("/authentication/form") //登录提交action
.successHandler(myAuthenticationSuccessHandler) //自定义的认证成功处理器
.failureHandler(myAuthenticationFailureHandler)//自定义的认证失败的处理器
.and()
.authorizeRequests() //下边的都是授权的配置
.antMatchers(securityProperties.getBrowser().getLoginPage()).permitAll() //放过登录页不过滤
.anyRequest() //任何请求
.authenticated() //都需要身份认证
.and()
.csrf().disable();
}
}
登录失败处理器和登录成功类似,自定义失败处理类,实现 AuthenticationFailureHandler 接口即可:
/**
* 认证失败处理器
*/
@Component("/myAuthenticationFailureHandler")
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
private Logger logger = LoggerFactory.getLogger(getClass());
//springmvc启动会自动注册一个ObjectMapper
@Autowired
private ObjectMapper objectMapper;
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
logger.info("登录失败");
//把authentication返回给响应
//状态码500,服务器内部错误
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(exception.getMessage()));
}
}
这样就可以在登录成功和失败后进入自己的处理逻辑了。