Spring|Spring Boot(六):集成Spring Security

前言:
百度百科中是这样解释Spring Security的:
Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反转Inversion of Control ,DI:Dependency Injection 依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。
对于我的理解:
Spring Security可以集中一个权限控制系统,可以用来保护 Web 应用的安全;
核心功能是:

  • 认证(你是谁)
  • 授权(你能干什么)
  • 攻击防护(防止伪造身份)
集成Spring Security步骤:
一、maven中添加依赖
org.springframework.boot spring-boot-starter-security

这个是最首要,也是最关键的;配置好后,直接运行项目,打开浏览器,访问项目上的接口时,你会发现,访问不了,直接进入了一个登入页面,如下图:

Spring|Spring Boot(六):集成Spring Security
文章图片
图片.png
原因:因为在SpringBoot中,默认的Spring Security就是生效了的,此时的接口都是被保护的,我们需要通过验证才能正常的访问。
Spring Security提供了一个默认的用户,用户名是user,而密码则是启动项目的时候自动生成的。
我们查看项目启动的日志,会发现如下的一段Log:
Spring|Spring Boot(六):集成Spring Security
文章图片
图片.png
每个人看到的password肯定是不一样的,我们直接用user和启动日志中的密码进行登录。
登录成功后,就跳转到了接口正常调用的页面了。
如果你不想一开始就使能Spring Security,怎么办呢;
之前有人告诉我,在application配置中直接加: security.basic.enabled=false 即可;
不行!不行!不行!是真的不行呀,根本没有关闭Spring Security,查了下资料:
只有在spring boot1.5配置security关闭http基本验证,可以在application配置中增加;security.basic.enabled=false进行关闭,但是在spring boot 2.0+之后这样配置就不能生效了,下面的配置文件讲解中会降到怎么进行关闭,^- ^;
二、对SpringSecurity进行相应的配置
新建配置类:SecurityConfig,继承WebSecurityConfigurerAdapter类,然后重写父类中的configure(HttpSecurity http) 方法。如下
/** * @author AxeLai * @date 2019-04-30 15:15 */ @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter {@Override protected void configure(HttpSecurity http) throws Exception { //配置需要登入验证 http.formLogin()// 定义当需要用户登录时候,转到的登录页面。 .and() .authorizeRequests()// 定义哪些URL需要被保护、哪些不需要被保护 .anyRequest()// 任何请求,登录后可以访问 .authenticated(); //配置不需要登入验证 //http.authorizeRequests() //.anyRequest() //.permitAll() //.and() //.logout() //.permitAll(); } }

上面代码中,注释‘配置需要登入验证’,打开‘配置不需要登入验证’就解决了security的关闭问题^- ^.
如果你只是为了测试Security的安全验证功能,上面这些就足够了;但是,如果需要进行登入的用户身份认证,权限设置等,还就需要接着往下看了:
三、配置用户认证逻辑
因为我们是要有自己的一套用户体系的,所以要配置用户认证:
新建LoginFilter配置类,继承UserDetailsService类:
package com.example.demo.loginfilter; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Component; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * @author AxeLai * @date 2019-05-01 14:01 */ @Component public class LoginFilterimplements UserDetailsService { private Logger logger = LoggerFactory.getLogger("adminLogger"); @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { logger.info("用户的用户名: {}", username); // TODO 根据用户名,查找到对应的密码,与权限 // 封装用户信息,并返回。参数分别是:用户名,密码,用户权限 User user = new User(username, "123456", AuthorityUtils.commaSeparatedStringToAuthorityList("admin")); return user; } }

【Spring|Spring Boot(六):集成Spring Security】这里我们没有进行过多的校验,用户名可以随意的填写,但是密码必须得是“123456”,这样才能登录成功。
同时可以看到,这里User对象的第三个参数,它表示的是当前用户的权限,我们将它设置为”admin”。
运行一下程序进行测试

Spring|Spring Boot(六):集成Spring Security
文章图片
图片.png
输入随意的用户和密码123456,发现根本登入不了;
后台报错:java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"

Spring|Spring Boot(六):集成Spring Security
文章图片
图片.png
下面说下原因:
这是因为Spring boot 2.0.3引用的security 依赖是 spring security 5.X版本,此版本需要提供一个PasswordEncorder的实例,否则后台汇报错误:
java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"
并且页面毫无响应。
更改:
把SecurityConfig改为:
package com.example.demo.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; /** * @author AxeLai * @date 2019-04-30 15:15 */ @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter {@Bean public PasswordEncoder passwordEncoder(){ //当然,如果你有自己的加密方法,这个方法就写自己的加密方法好了 return new BCryptPasswordEncoder(); }@Override protected void configure(HttpSecurity http) throws Exception { //配置需要登入验证 http.formLogin()// 定义当需要用户登录时候,转到的登录页面。 .and() .authorizeRequests()// 定义哪些URL需要被保护、哪些不需要被保护 .anyRequest()// 任何请求,登录后可以访问 .authenticated(); //配置不需要登入验证 //http.authorizeRequests() //.anyRequest() //.permitAll() //.and() //.logout() //.permitAll(); } }

把LoginFilter改为:
package com.example.demo.loginfilter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Component; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * @author AxeLai * @date 2019-05-01 14:01 */ @Component public class LoginFilterimplements UserDetailsService { @Autowired private PasswordEncoder passwordEncoder; private Logger logger = LoggerFactory.getLogger("adminLogger"); @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { logger.info("用户的用户名: {}", username); // TODO 根据用户名,查找到对应的密码,与权限 // 封装用户信息,并返回。参数分别是:用户名,密码,用户权限 User user = new User(username, passwordEncoder.encode("123456"), AuthorityUtils.commaSeparatedStringToAuthorityList("admin")); return user; } }

重启项目,随便的用户名+密码123456登入,你就发现登入已经成功了 ,直接进入我们项目的首页,后台会获取到你的用户名:

Spring|Spring Boot(六):集成Spring Security
文章图片
图片.png
讲解:
1.当我们写LoginFilter 类时;里面实现了一个方法loadUserByUsername,并返回了一个UserDetails。这个UserDetails 就是封装了用户信息的对象,里面包含了七个方法:
// // Source code recreated from a .class file by IntelliJ IDEA // (powered by Fernflower decompiler) //package org.springframework.security.core.userdetails; import java.io.Serializable; import java.util.Collection; import org.springframework.security.core.GrantedAuthority; public interface UserDetails extends Serializable { Collection getAuthorities(); String getPassword(); String getUsername(); boolean isAccountNonExpired(); boolean isAccountNonLocked(); boolean isCredentialsNonExpired(); boolean isEnabled(); }

我们在返回UserDetails的实现类User的时候,可以通过User的构造方法,设置对应的参数就可以;
2.SpringSecurity中有一个PasswordEncoder接口:
// // Source code recreated from a .class file by IntelliJ IDEA // (powered by Fernflower decompiler) //package org.springframework.security.crypto.password; public interface PasswordEncoder { String encode(CharSequence var1); boolean matches(CharSequence var1, String var2); default boolean upgradeEncoding(String encodedPassword) { return false; } }

我们只需要自己实现这个接口,并在配置文件中配置一下就可以了。
上面我只是暂时以默认提供的一个实现类进行测试:
@Bean public PasswordEncoder passwordEncoder(){ //当然,如果你有自己的加密方法,这个方法就写自己的加密方法好了 return new BCryptPasswordEncoder(); }

四、个性化用户认证逻辑
在上面的测试中,一直都是使用的默认的登录界面,我相信每个产品都是有自己的登录界面设计的,所以我们这一节了解一下如何自定义登录页面。
我们先写一个简单的登录页面login.html:
登录页面 - 锐客网 自定义登录页面
用户名:
密码:

完成了登录页面之后,就需要将它配置进行SecurityConfig进行更改:
//配置需要登入验证的自定义配置 http.formLogin()// 定义当需要用户登录时候,转到的登录页面。 .loginPage("/login.html")// 设置登录页面 .loginProcessingUrl("/user/login") // 自定义的登录接口 .and() .authorizeRequests()// 定义哪些URL需要被保护、哪些不需要被保护 .antMatchers("/login.html").permitAll()// 设置所有人都可以访问登录页面 .anyRequest()// 任何请求,登录后可以访问 .authenticated() .and() .csrf().disable(); // 关闭csrf防护

这样,每当我们访问被保护的接口的时候,就会调转到login.html页面,如下:

Spring|Spring Boot(六):集成Spring Security
文章图片
图片.png
最后附上项目结构图:
Spring|Spring Boot(六):集成Spring Security
文章图片
图片.png
代码已上传到: https://github.com/AxeLai/SpringBoot-.git
OVER IS NOT OVER!
番外
上面这些,是在前后端不分离的情况下进行开发编写的,现如今一般项目都前后端分离了,后端提供接口供前端调用,返回JSON格式的数据给前端。刚才那样,调用了被保护的接口,直接进行了页面的跳转,这样执行不太友好。
处理:在前后端分离的情况下,我们登录成功了直接向前端返回用户的个人信息,而不是直接进行跳转。登录失败也是同样的道理。
这里涉及到了Spring Security中的两个接口AuthenticationSuccessHandler和AuthenticationFailureHandler。我们可以实现这个接口,并进行相应的配置就可以了。 当然框架是有默认的实现类的,我们可以继承这个实现类再来自定义自己的业务:
@Component("myAuthenctiationSuccessHandler") public class MyAuthenctiationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { private Logger logger = LoggerFactory.getLogger(getClass()); @Autowired private ObjectMapper objectMapper; @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,Authentication authentication) throws IOException, ServletException { logger.info("登录成功"); response.setContentType("application/json; charset=UTF-8"); response.getWriter().write(objectMapper.writeValueAsString(authentication)); } }

这里我们通过response返回一个JSON字符串回去。
这个方法中的第三个参数Authentication,它里面包含了登录后的用户信息(UserDetails),Session的信息,登录信息等。
@Component("myAuthenctiationFailureHandler") public class MyAuthenctiationFailureHandler extends SimpleUrlAuthenticationFailureHandler { private Logger logger = LoggerFactory.getLogger(getClass()); @Autowired private ObjectMapper objectMapper; @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { logger.info("登录失败"); response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); response.setContentType("application/json; charset=UTF-8"); response.getWriter().write(objectMapper.writeValueAsString(new BaseResponse(exception.getMessage()))); } }

这个方法中的第三个参数AuthenticationException,包括了登录失败的信息。
同样的,还是需要在配置文件中进行配置,这里就不贴出全部的代码了,只贴出相应的语句
.successHandler(myAuthenticationSuccessHandler) // 自定义登录成功处理 .failureHandler(myAuthenticationFailureHandler) // 自定义登录失败处理

OVER IS OVER!

    推荐阅读