SpringSecurity
SpringSecurity简介
安全框架概述
什么是安全框架?解决系统安全问题的框架。如果没有安全框架,我们需要手动处理每个资源的访问控制,非常麻烦。使用安全框架,我们可以通过配置的方式实现对资源的访问限制。
常用的安全框架
-
Spring Security:Spring家族中的一员。是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置Bean,充分利用了Spring IoC,DI和AOP功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。
-
Apache Shiro:一个功能强大且易于使用的Java安全框架,提供了认证,授权,加密和会话管理等功能。
Spring Security简介
Spring Security是一个高度自定义的安全框架。利用Spring IoC/DI和AOP功能,为系统提供了声明式安全访问控制功能,减少了为系统安全而编写的大量重复代码。使用Spring Security的原因很多,但大部分都是发现了JavaEE的Servlet规范或EJB规范中的安全功能缺乏典型企业应用场景。同时认识到他们在WAR和EAR级别无法移植。因此如果你更换服务器环境,还有大量工作去重新配置你的应用程序。使用Spring Security解决了这些问题,也为你提供了许多其他有用的、可定制的安全功能。正如你可能知道的一个应用程序的两个主要区域是“认证”和“授权”。这两点也是SpringSecurity的核心功能。“认证”是通俗点说就是认为用户是否登录。“授权”通俗点讲就是系统判断用户是否有权限去做某些事情。
引入Spring Security 依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
当我们在Spring Boot项目中引入 Spring Security 依赖后对项目不做任何配置。那么我们要访问项目的某个地址,如:/login,就会出现如下界面:
说明,Spring Security已经生效。这是Spring Security默认的登录逻辑生效了。输入用户user,密码为控制台打印的密码,即可登录。如果我们不想用Spring Security的默认登录逻辑,那么我们就需要自定义登录逻辑。
使用SpringSecurity 自定义登录逻辑
-
在 Spring 的 IoC 容器中,放入一个 PassworEncoder 对象,官方推荐使用 BCryptPasswordEncoder 密码加密器。
@Configuration public class SecurityConfig { @Bean public PasswordEncoder getPwd() { return new BCryptPasswordEncoder(); } }
-
实现 UserDetailsService 接口
-
UserDetailsService接口的源码
public interface UserDetailsService { /** * Locates the user based on the username. In the actual implementation, the search * may possibly be case sensitive, or case insensitive depending on how the * implementation instance is configured. In this case, the <code>UserDetails</code> * object that comes back may have a username that is of a different case than what * was actually requested.. * @param username the username identifying the user whose data is required. * @return a fully populated user record (never <code>null</code>) * @throws UsernameNotFoundException if the user could not be found or the user has no * GrantedAuthority */ UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; }
该接口定义 loadUserByUsername方法,需要username参数,该参数就是前端传入的一个用户名。返回UserDetails 对象。接下来我们看一下UserDetails接口。
-
UserDetails接口源码
public interface UserDetails extends Serializable { /** * Returns the authorities granted to the user. Cannot return <code>null</code>. * @return the authorities, sorted by natural key (never <code>null</code>) */ Collection<? extends GrantedAuthority> getAuthorities(); /** * Returns the password used to authenticate the user. * @return the password */ String getPassword(); /** * Returns the username used to authenticate the user. Cannot return * <code>null</code>. * @return the username (never <code>null</code>) */ String getUsername(); /** * Indicates whether the user's account has expired. An expired account cannot be * authenticated. * @return <code>true</code> if the user's account is valid (ie non-expired), * <code>false</code> if no longer valid (ie expired) */ boolean isAccountNonExpired(); /** * Indicates whether the user is locked or unlocked. A locked user cannot be * authenticated. * @return <code>true</code> if the user is not locked, <code>false</code> otherwise */ boolean isAccountNonLocked(); /** * Indicates whether the user's credentials (password) has expired. Expired * credentials prevent authentication. * @return <code>true</code> if the user's credentials are valid (ie non-expired), * <code>false</code> if no longer valid (ie expired) */ boolean isCredentialsNonExpired(); /** * Indicates whether the user is enabled or disabled. A disabled user cannot be * authenticated. * @return <code>true</code> if the user is enabled, <code>false</code> otherwise */ boolean isEnabled(); }
该接口中定义了:
- 获取权限的方法
- 获取用户名密码的方法
- 判断账户是否被锁
- 判断账户是否过期
- 判断凭证(密码)是否过期
- 判断账户是否可以认证
因为UserDetails是一个接口,所以必定有实现类。我们可以发现:org.springframework.security.core.userdetails.User 类实现了UserDetails接口,并且我们也常用该对象作为 UserDetailsService 的实现类中loadUserByUsername方法的返回值。
-
实现UserDetailsService接口
@Service public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private PasswordEncoder passwordEncoder; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { if (!"admin".equals(username)){ throw new UsernameNotFoundException("username is not found"); } String password = passwordEncoder.encode("123"); return new User(username, password, AuthorityUtils.commaSeparatedStringToAuthorityList( "admin,normal" )); } }
实现该接口的loadUserByUsername方法,实现逻辑如下:
- 明确Username是来自前端
- 根据Username去查询数据库,获取到加密后的密码 和 当前用户对应的权限列表,并放回给User对象。
为了简便,上面的代码省略了查询数据库的操作。
-
如果不想使用SpringSecurity提供的登录页面,需要继承WebSecurityConfigurerAdapter类,重写configure(HttpSecurity http)方法。我们可以让之前的 SecurityConfig 继承WebSecurityConfigurerAdapter。
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.formLogin() .loginPage("/login.html"); // 自定义的登录页面,这样就会覆盖掉SpringSecurity默认的登录页面了 } @Bean public PasswordEncoder getPwd() { return new BCryptPasswordEncoder(); } }
login.html页面代码
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <form action="/login" method="post"> <!--这里的登录方式,必须是post--> <input type="text" name="username"/><br/> <!-- 这里的name属性必须是username--> <input type="password" name="password"/><br/> <!--这里的name属性必须是password --> <input type="submit" value="submit"> </form> </body> </html>
-
当我们重写了 protected void configure(HttpSecurity http) 方法之后,我们访问http://localhost:8080/login.html页面(login.html放在了项目的resource/static/目录下),就不会再去到Spring Security的默认登录页面。但是,当我们访问http://localhost:8080/main.html(main.html在项目的resource/static/目录下)这个页面的时候,也直接跳转到了该页面。这与授权逻辑相关:
@Override protected void configure(HttpSecurity http) throws Exception { http.formLogin() .loginPage("/login.html"); http.authorizeRequests() .antMatchers("/login.html").permitAll() //对login.html的请求,放行,不需要认证 .anyRequest().authenticated(); // 其他任何请求都需要认证 }
将configure方法的逻辑修改为如上。增加除/login.html请求外,其他所有请求都要进行认证的逻辑。此时我们再访问main.html页面,就会跳转到login.html页面,先登录才行。
-
现在输入 http://localhost:8080/main.html页面,会跳转到自定义登录页面。
当点击提交按钮的时候,我们发现没有返回到main.html页面,还是停留在login.html页面了。我们需要重写修改configure代码:@Override protected void configure(HttpSecurity http) throws Exception { http.formLogin() .loginProcessingUrl("/login") // 这里/login不是随便写的,要与login.html页面的form表单的action属性的值一样 .loginPage("/login.html"); http.authorizeRequests() .antMatchers("/login.html").permitAll() .anyRequest().authenticated(); http.csrf().disable(); // 关闭 csrf
-
当我们输入正确的用户名密码后,我们到达了main.html页面。(我用的是Spring Boot 2.6.3 版本),其他版本有可能跳转不过去,此时我们需要修改configure代码为如下:
@Override protected void configure(HttpSecurity http) throws Exception { http.formLogin() .loginProcessingUrl("/login") .loginPage("/login.html") .successForwardUrl("/toMain"); // 这里默认发出的是Post请求, 登录成功后跳转的url,需要定义controller,重定向到main.html http.authorizeRequests() .antMatchers("/login.html").permitAll() .anyRequest().authenticated(); http.csrf().disable(); }
增加 跳转的controller
@Controller public class LoginController { @RequestMapping("toMain") public String toMain(){ System.out.println("exec main method"); return "redirect:main.html"; } }
-
当我们输入的用户名密码错误时,我们会停留在login.html页面,但是url会变为http://localhost:8080/login.html?error,如果我们想自定义登录失败页面,需要这样做:
-
在resource/static/目录下,增加失败页面 error.html
-
修改protected void configure(HttpSecurity http) 代码如下:
@Override protected void configure(HttpSecurity http) throws Exception { http.formLogin() .loginProcessingUrl("/login") .loginPage("/login.html") .successForwardUrl("/toMain") .failureForwardUrl("/toError"); // 登录失败后的跳转逻辑 http.authorizeRequests() .antMatchers("/error.html").permitAll() // 这里要放行/error.html请求,否则也需要登录,那么就会重新回到login.html页面了 .antMatchers("/login.html").permitAll() .anyRequest().authenticated(); http.csrf().disable(); }
增加controller代码
@RequestMapping("toError") public String toError(){ System.out.println("exec error method"); return "redirect:error.html"; }
-
-
前面我们的login.html页面,用户名,密码输入框的name属性值必须是username和password,并且登录方式必须是post。为什么呢?原因是:当我们登录时,执行我们自定义登录逻辑时,需要先执行SpringSecurity的 UsernamePasswordAuthenticationFilter g过滤器。查看过滤器的源码,我们发现,源代码里是这样规定的:
public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username"; // 需要前端表单的用户名为username public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password"; // 需要前端表单的密码为password private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/login", "POST"); private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY; private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY; private boolean postOnly = true; public UsernamePasswordAuthenticationFilter() { super(DEFAULT_ANT_PATH_REQUEST_MATCHER); } @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { if (this.postOnly && !request.getMethod().equals("POST")) { throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod()); } String username = obtainUsername(request); username = (username != null) ? username : ""; username = username.trim(); String password = obtainPassword(request); password = (password != null) ? password : ""; UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password); // Allow subclasses to set the "details" property setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); }
-
如果我们想修改默认的用户名和密码参数,可以修改configure类,如下:
@Override protected void configure(HttpSecurity http) throws Exception { http.formLogin() .usernameParameter("username123") // 自定义用户名参数 .passwordParameter("password123") // 自定义密码参数 .loginProcessingUrl("/login") .loginPage("/login.html") .successForwardUrl("/toMain") .failureForwardUrl("/toError"); http.authorizeRequests() .antMatchers("/error.html").permitAll() .antMatchers("/login.html").permitAll() .anyRequest().authenticated(); http.csrf().disable(); }
并且修改登录页面,如下:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <form action="/login" method="post"> <!--这里的登录方式,必须是post--> <input type="text" name="username123"/><br/> <!-- 这里name属性使用自定义的username123--> <input type="password" name="password123"/><br/> <!--这里的name属性使用自定义的password123 --> <input type="submit" value="submit"> </form> </body> </html>
-
在前后端分离的项目中,我们不会使用 .successForwardUrl("/toMain") 和 .failureForwardUrl("/toError")并自定义controller的方式来实现跳转的,这种方式适用于项目内部。如果想实现项目外部的一个跳转,怎么办呢?
通过successForwardUrl的源码,我们发现,ForwardAuthenticationSuccessHandler类,该类实现了AuthenticationSuccessHandler接口,实现内部的请求转发的。我们要实现一个外部的请求转发,显然需要实现AuthenticationSuccessHandler接口。public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler { private String url; public MyAuthenticationSuccessHandler(String url) { this.url = url; } @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { User user = (User)authentication.getPrincipal(); // System.out.println(user.getUsername()); // 用户名 System.out.println(user.getPassword()); // 密码,考虑到安全的原因,这里的密码获取到的是null System.out.println(user.getAuthorities()); // 该用户的权限信息 response.sendRedirect(url); } }
修改configure方法,使用我们自定义的MyAuthenticationSuccessHandler。
@Override protected void configure(HttpSecurity http) throws Exception { http.formLogin() .usernameParameter("username123") .passwordParameter("password123") .loginProcessingUrl("/login") .loginPage("/login.html") //.successForwardUrl("/toMain") 这行代码与successHandler不能共存,因此注释掉 .successHandler(new MyAuthenticationSuccessHandler("http://www.baidu.com")) // 使用自定义的请求转换器 .failureForwardUrl("/toError"); http.authorizeRequests() .antMatchers("/error.html").permitAll() .antMatchers("/login.html").permitAll() .anyRequest().authenticated(); http.csrf().disable(); }
同理:我们可以自定义登录失败的处理,实现登录失败后的逻辑处理。这样我们原理处理跳转成功或失败的controller代码便不再使用了。