Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架
。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反转Inversion of Control ,DI:Dependency Injection 依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作,就是一个授权(Authorization)和用户认证(Authentication)的安全框架。
SpringSecurity 官方文档:https://docs.spring.io/spring-security/reference/getting-spring-security.html
导入依赖:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
dependency>
创建一个初始化的SpringBoot项目,导入对应的依赖(web依赖、springsecurity依赖等),然后直接启动项目,当访问项目的某个资源时,会跳转到一个登陆页面。
这个是springsecurity默认的登录页面,请求项目的资源时,如果没有登录就不会让你进行访问,而初始的登录账户是user,密码是项目启动时打印的,每次启动项目的密码不同。
如果需要自定义一个自己的登录逻辑,我们需要自己创建一个UserDetailsServiceImpl
来实现UserDetailsService
接口,然后重写loadUserByUsername
方法,自定义登录逻辑。
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
// loadUserByUsername() username参数是前端传来的登录账户参数
// UserDetails 返回参数,也是一个接口,但是返回这个接口的实现类User
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return null;
}
}
public interface UserDetails extends Serializable {
// 返回用户的权限集合,但是不能返回一个null
Collection<? extends GrantedAuthority> getAuthorities();
// 获取密码
String getPassword();
// 获取账号
String getUsername();
// 判断用户账号是否过期
boolean isAccountNonExpired();
// 判断用户是否被锁定
boolean isAccountNonLocked();
// 判断用户密码是否过期
boolean isCredentialsNonExpired();
// 判断账户是否可用
boolean isEnabled();
}
这是一个密码加密和匹配的一个接口,这就接口有很多实现类,对应的就是各种加密算法,但是官方推荐使用BCryptPasswordEncoder()
, 一般会把这个实现类注入到spring
容器中。
@Configuration
public class SecurityConfig {
@Bean
public PasswordEncoder getPasswordEncoder(){
return new BCryptPasswordEncoder();
}
}
public interface PasswordEncoder {
// 将一个字符串进行加密
String encode(CharSequence rawPassword);
// 第一个参数是原始的密码,第二个参数是加密后的算法,如果匹配就返回true,否则返回false
boolean matches(CharSequence rawPassword, String encodedPassword);
default boolean upgradeEncoding(String encodedPassword) {
return false;
}
}
// 需要注入到spring容器中
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private PasswordEncoder passwordEncoder;
// loadUserByUsername() username参数是前端传来的登录账户参数,前端的参数名必须为username,不能改变
// UserDetails 返回参数,也是一个接口,但是返回这个接口的实现类User
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 1、 通过username从数据库中查询数据
Admin admin = new Admin(1,"admin","123456"); // 模拟查询的数据
if (admin==null){
// 如果不存在就抛出异常
throw new UsernameNotFoundException("用户名不存在");
}
// 返回一个user对象,第一个参数是参数username,
// 第二个参数是数据库中查询的密码,这个参数必须通过passwordEncoder加密,不然也不会通过,数据库保存的密码一般都是加密了的,不会是明文密码
// 第三个参数权限列表
return new User(username,passwordEncoder.encode(admin.getAdminPassword()),
AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
}
}
如果没有设置自定义的登录页面,就会使用springsecurity
默认的登录页面。
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter{
@Bean
public PasswordEncoder getPasswordEncoder(){
return new BCryptPasswordEncoder();
}
// 权限分配
@Override
protected void configure(HttpSecurity http) throws Exception {
// 拦截所有的请求
http.authorizeRequests()
// 为某些请求设置为所有人都可以访问
.antMatchers("/","/index.html","/login.html","/rest.html","/assets/**","/fail.html").permitAll()
// 任何请求都需要认证
.anyRequest().authenticated();
// 登录表单
http.formLogin()
// 自定义登录页面
.loginPage("/login.html")
// 设置前端的传入登录名的参数名,默认是username,可以修改为其他但是和前端对应
.usernameParameter("user").passwordParameter("password")
// 设置登录请求,如果是/login就认为是登录请求
.loginProcessingUrl("/myLogin")
// 登录成功的跳转页面
/*
设置登录成功的页面:
方式一: defaultSuccessUrl("url",true),如果不设置第二个参数为true的话,如果访问的访问的是一个不存在的页面
登录成功就会跳到这个不存在的url。
方式二: successForwardUrl("url"),需要一个post请求,在MvcConfig设置的请求都是get请求,
需要写一个controller重定向到对应的页面。
*/
.successForwardUrl("/toMain")
// 登录失败的跳转页面,注意登录失败的请求也会被认证,所以需要将失败的请求的认证放行
// 还有一个failureUrl()与failureForwardUrl()的区别在于,前者需要一个get请求,而后者需要一个post请求
// .failureUrl("/fail.html")
.failureForwardUrl("/failLogin");
// 关闭csrf防御
http.csrf().disable();
}
}
// 登录成功的处理器
.successHandler(new AuthenticationSuccessHandler() {
// request、response
// Authentication用户认证,可获得登录账号、密码和权限集合
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
// 重定向到指定页面
response.sendRedirect(request.getContextPath()+"/main.html");
}
})
// 登录失败的处理器
.failureHandler(new AuthenticationFailureHandler() {
// request、response
// exception异常处理
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
logger.info(exception);
request.setAttribute("msg","登录失败!!");
response.sendRedirect(request.getContextPath()+"/fail.html");
}
});
处理器只能通过response
请求从定向到某个页面,不能使用请求转发,只支持get
请求的方法。
编写一个配置类SecurityConfig
,这个继承 WebSecurityConfigurerAdapter
类,然后重载这个类的方法。
@EnableWebSecurity // 这个注解开启了Security
public class SecurityConfig extends WebSecurityConfigurerAdapter {
}
// 授权配置,链式编程
@Override
protected void configure(HttpSecurity http) throws Exception {
// 拦截所有的请求
http.authorizeRequests()
// 为某些请求设置为所有人都可以访问
.antMatchers("/","/index.html","/login.html","/rest.html","/assets/**","/fail.html").permitAll()
// regexMatchers()通过正则表达式来匹配请求,
// 第一个参数是请求方式,是一个enum类型,可以省略,省略后就是所有访问方法都可以访问
// 不省略就是指定的访问方法才能访问
.regexMatchers(HttpMethod.GET,".+[.]jpg").permitAll()
// 表示这个请求时拥有root这个权限的才能访问,权限名严格区分大小写
.antMatchers("/manager/**").hasAuthority("root")
// 表示这个请求需要admin或者root的权限才能进入,参数是可变长参数
.antMatchers("/user/**").hasAnyAuthority("admin","root")
// 任何请求都需要认证
.anyRequest().authenticated();
// 没有权限,自动默认的登录请求
http.formLogin();
}
// 6种内置的授权访问方法
/*
// 所有人都可以访问
static final String permitAll = "permitAll";
// 所有人都不允许访问
private static final String denyAll = "denyAll";
// 匿名访问,和permitAll()类似
private static final String anonymous = "anonymous";
// 所有请求需要认证后才能访问
private static final String authenticated = "authenticated";
// 全认证访问,只能通过输入账号和密码登录后才能访问,不能通过记住我来访问
private static final String fullyAuthenticated = "fullyAuthenticated";
// 通过记住我可以进行访问
private static final String rememberMe = "rememberMe";
*/
一个系统中拥有多种角色,需要根据不同的角色让其页面的展示效果不同,首先对登录的用户进行配置角色,然后判断角色能否访某个请求。
在UserDetailsServiceImpl
中授予用户角色。
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 1、 通过username从数据库中查询数据
User user = new User(null, username, null);
List<User> users = userMapper.queryUserByPojo(user);
if (users.size()==0){
// 如果不存在就抛出异常
throw new UsernameNotFoundException("用户名不存在");
}
// 返回一个user对象,第一个参数是参数username,
// 第二个参数是数据库中查询的密码,这个参数必须通过passwordEncoder加密,不然也不会通过,数据库保存的密码一般都是加密了的,不会是明文密码
// 第三个参数权限列表,角色需要使用ROLE_开头表示这是一个角色,每个值之间使用逗号隔开
return new org.springframework.security.core.userdetails.User(username,users.get(0).getPassword(),
AuthorityUtils.commaSeparatedStringToAuthorityList("root,ROLE_用户"));
}
请求角色的判断:
// 拦截所有的请求
http.authorizeRequests()
// 为某些请求设置为所有人都可以访问
.antMatchers("/","/index.html","/login.html","/rest.html","/assets/**","/fail.html").permitAll()
// regexMatchers()通过正则表达式来匹配请求,
// 第一个参数是请求方式,是一个enum类型,可以省略,省略后就是所有访问方法都可以访问
// 不省略就是指定的访问方法才能访问
.regexMatchers(HttpMethod.GET,".+[.]jpg").permitAll()
// // 表示这个请求时拥有root这个权限的才能访问,权限名严格区分大小写
// .antMatchers("/manager/**").hasAuthority("root")
// // 表示这个请求需要admin和root的权限才能进入,参数是可变长参数
// .antMatchers("/user/**").hasAnyAuthority("admin","root")
// 表示这个请求需要拥有这个管理员角色才能访问,这是的角色不能加ROLE_为开头
.antMatchers("/manager/**").hasRole("管理员")
// 表示这个请求需要拥有这个管理员或者用户角色才能访问
.antMatchers("/user/**").hasAnyRole("管理员","用户")
// 任何请求都需要认证
.anyRequest().authenticated();
springsecurity
提供了记住我功能,通过简单的配置就能够实现。如果用户在登录的时候选择了记住我功能后,用户再次访问网站时springsecurity
就帮我们从数据库中获取数据自动登录,所以需要使用到数据库的连接。
在SecurityConfig
配置类中,进行以下配置:
// 记住我的PersistentTokenRepository注入
@Bean
public PersistentTokenRepository getPersistentTokenRepository(){
// 创建一个jdbcToken对象
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
// 设置数据源
jdbcTokenRepository.setDataSource(druidDataSource);
// 在第一次启动时创建一个表,用户存放数据,第二次启动时需要删除
// jdbcTokenRepository.setCreateTableOnStartup(true);
return jdbcTokenRepository;
}
在protected void configure(HttpSecurity http)
这个方法中开启记住我功能:
// 记住我功能
http.rememberMe()
// 设置前端的参数名,默认是remember-me
.rememberMeParameter("remember")
// 设置记住我功能的期限,默认是14天,单位是秒
.tokenValiditySeconds(60*60*60)
// 配置自定义登录逻辑
.userDetailsService(userDetailsService)
// 配置token
.tokenRepository(this.getPersistentTokenRepository());
当每登录一次在数据库中新创建的表就会多一条记录:
导入依赖:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-thymeleafartifactId>
dependency>
<dependency>
<groupId>org.thymeleaf.extrasgroupId>
<artifactId>thymeleaf-extras-springsecurity5artifactId>
dependency>
导入thymeleaf命名空间:
xmlns="http://www.w3.org/1999/xhtml"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5"
<div>
登录账号:<span sec:authentication="name">span><br>
登录账号:<span sec:authentication="principal.username">span><br>
凭证:<span sec:authentication="credentials">span><br>
权限和角色:<span sec:authentication="authorities">span><br>
客户端地址:<span sec:authentication="details.remoteAddress">span><br>
sessionId:<span sec:authentication="details.sessionId">span><br>
div>
<div>
是否登录:<span sec:authorize="isAuthenticated()">span>
<button sec:authorize="hasAuthority('root')">删除button>
<button sec:authorize="hasAnyAuthority('admin','root')">查看button>
<button sec:authorize="hasRole('超级管理员')">我是超级管理员button>
<button sec:authorize="hasAnyRole('超级管理员','管理员')">我是超级管理员和管理员button>
div>
// 退出功能
http.logout()
// 设置退出的请求url,默认是/logout
.logoutUrl("/user/logout")
// 设置退出成功后的跳转页面
.logoutSuccessUrl("/login.html");
The default is that accessing the URL “/logout” will log the user out by invalidating the HTTP Session, cleaning up any rememberMe() authentication that was configured, clearing the SecurityContextHolder, and then redirect to “/login?success”。可以请求/logout
,然后就会清除session,然后重定向到/login?success
。
<li> <a th:href="@{/user/logout}"> <i class="fa fa-lock">i> 退出系统 a> li>
CSRF(Cross-site request forgery),也被称为:one click attack/session riding,中文名称:跨站请求伪造,缩写为:CSRF/XSRF。
一般来说,攻击者通过伪造用户的浏览器的请求,向访问一个用户自己曾经认证访问过的网站发送出去,使目标网站接收并误以为是用户的真实操作而去执行命令。常用于盗取账号、转账、发送虚假消息等。攻击者利用网站对请求的验证漏洞而实现这样的攻击行为,网站能够确认请求来源于用户的浏览器,却不能验证请求是否源于用户的真实意愿下的操作行为。在springsecurity4以后就有了csrf防御,默认是开启的,但是在学习阶段都是关闭了csrf功能。
// 关闭csrf防御
http.csrf().disable();
springsecurity4
以后为了防止crsf
攻击,保证不是第三方网站访问,要求在访问时需要携带参数名为_csrf
值为token
的内容,token
是服务器生成的。如果值和服务器的值得token
匹配就允许访问。例如已登录为例,可以使用隐藏域来传入一个token值:
<input type="hidden" th:value="${_csrf}" name="_csrf" th:if="${_csrf}">