SpringBoot19 集成SpringSecurity01 -> 环境搭建、SpringSecurity验证、SpringSecurity配置进阶、登录页面处理
1 环境搭建
1.1 创建一个SpringBoot项目
1.2 创建一个Restful接口
新建一个Controller类即可
package com.example.wiremock.controller; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * @author 王杨帅 * @create 2018-05-12 21:18 * @desc **/ @RestController @RequestMapping(value = "/security") @Slf4j public class SecurityController { @GetMapping(value = "/connect") public String connect() { String result = "前后台连接成功"; log.info("===" + result); return result; } }
1.3 引入SpringSecurity相关jar包
<!--security相关--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
1.4 启动项目
技巧01:由于我们引入了SpringSecurity相关的jar包,所以系统会默认开启SpringSecurity的相关配置
技巧02:可以在配置文件中关掉这个配置(即:使SpringSecurity失效)
技巧03:启动项目后会在控制台上打印出一个密码,因为默认的SpringSecurity配置会对所有的请求都进行权限验证,如果不通过就会跳转到 /login 请求,则是一个登陆页面或者一个登陆弹出窗口,默认登陆名为 user,默认登陆密码就是启动项目是控制台打印出来的字符串
1.5 访问接口
IP + 端口 + 上下文p径 + 请求路径
http://127.0.0.1:9999/dev/security/connect
技巧01:SpringSecurity默认的配置会默认对所有的请求都进行权限验证,所以会跳转到 /login 请求路径,画面如下;输入正确的用户名和密码后跳转到之的请求所得到的响应
1.6 SpringSecurity的授权流程
所有请求url -> BasicAuthenticationFilter / UsernamePasswordAuthenticationFilter -> FilterSecurityInterceptor -> BasicAuthenticationFilter / UsernamePasswordAuthenticationFilter -> FilterSecurityInterceptor -> controller层
所有请求都默认进入 BasicAuthenticationFilter 过滤器进行过滤,然后进入 FilterSecurityInterceptor 过滤器进行权限验证,如果在 FilterSecurityInterceptor 中权限验证就会跳转到 /login 请求进行处理,然后在进入 BasicAuthenticationFilter 或者 UsernamePasswordAuthenticationFilter 过滤器,再进入 FilterSecurityInterceptor,只有当 FilterSecurityInterceptor 过滤通过了才会跳转到之前的请求路径
技巧01:如果在 FilterSecurityInterceptor 中抛出了异常就会跳转到 ExceptionTranslationFilter 进行相应的处理
2 SpringSecurity验证
直接使用SpringSecurity默认的配置进行权限验证时只有一个用户,无法满足实际开发需求;在实际的开发中需要根据不同的用户判断其权限
技巧01:直接继承一个UserDetailsService接口即可;该接口中有一个 loadUserByUsername 方法,该方法是通过用户名查找用户信息,然后在根据查到的用户信息来判断该用户的权限,该方法返回一个实现了UserDetailsService接口的User对象
2.1 实现 UserDetailsService
技巧01:实现了 UserDetailsService接口的实现类必须在类级别上添加@Component注解,目的上让Spring容器去管理这个Bean
技巧02:可以在实现了 UserDetailsService接口的实现类中依赖注入其他Bean(例如:依赖注入持久层Bean来实现数据库操作)
技巧03:如果实现了 UserDetailsService 接口就必须进行 SpringSecurity 配置,因为SpringSecurity会使用一个实现了PasswordEncoder接口的实现类去比较用户录入的密码和从数据库中获取到的密码是否相等
技巧04:实现了 UserDetailsService接口的实现类就是自定义的认证方式,需要将自定义的认证方式添加到认证管理构建中心,否则自定义认证方式不会生效
package com.example.wiremock.service; import lombok.extern.slf4j.Slf4j; 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.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Component; /** * @author 王杨帅 * @create 2018-05-12 22:09 * @desc **/ @Component @Slf4j public class FuryUserDetailService implements UserDetailsService { @Autowired private PasswordEncoder passwordEncoder; // 01 依赖注入持久层(用于查找用户信息) @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { log.info("用户名:" + username); // 技巧01: /login 请求传过来的用户名会传到这里 // 02 根据用户名到数据库中查找数据 String pwd = passwordEncoder.encode("123321"); // 技巧02:此处是模拟的从数据库中查询到的密码 // 03 返回一个 User 对象(技巧01:这个User对象时实现了UserDetail接口的,这里利用的是Spring框架提供的User对象,也可以使用自定义但是实现了UserDetai接口的User对象) return new User(username, pwd, AuthorityUtils.commaSeparatedStringToAuthorityList("admin")); } }
2.2 SpringSecurity配置
技巧01:其实就是配置一个Bean而已,只不过这个Bean的返回类型是 PasswordEncoder 类型
技巧02:可以使用实现了 PasswordEncoder接口的实现类 BCryptPasswordEncoder 作为返回类型,也可以使用自定义并且实现了 PasswordEncoder接口的类作为返回类型
package com.xunyji.springsecurity01.config; //import com.xunyji.springsecurity01.service.FuryUserDetailService; import com.xunyji.springsecurity01.service.FuryUserDetailService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; /** * @author 王杨帅 * @create 2018-09-07 22:18 * @desc **/ @Configuration //@EnableWebSecurity public class SpringSecurityConfig extends WebSecurityConfigurerAdapter{ @Autowired private FuryUserDetailService furyUserDetailService; @Autowired private PasswordEncoder passwordEncoder; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { // 内存认证 start // auth.inMemoryAuthentication() // .passwordEncoder(new MyPasswordEncoder()) // .withUser("admin") // .password("123321") // .roles("ADMIN"); // auth.inMemoryAuthentication() // .passwordEncoder(new MyPasswordEncoder()) // .withUser("wys") // .password("123321") // .roles("USER"); // 内存认证 end auth.authenticationProvider(authenticationProvider()); // 添加自定义的认证逻辑 } /** * 创建认证提供者Bean * 技巧01:DaoAuthenticationProvider是SpringSecurity提供的AuthenticationProvider实现类 * @return */ @Bean public DaoAuthenticationProvider authenticationProvider() { DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); // 创建DaoAuthenticationProvider实例 authProvider.setUserDetailsService(furyUserDetailService); // 将自定义的认证逻辑添加到DaoAuthenticationProvider authProvider.setPasswordEncoder(passwordEncoder); // 设置自定义的密码加密 return authProvider; } @Override public void configure(WebSecurity web) throws Exception { super.configure(web); } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/test/home").permitAll() .anyRequest().authenticated() .and() .logout().permitAll() .and() .formLogin(); http.csrf().disable(); // 关闭csrf验证 } /** * 创建PasswordEncoder对应的Bean * @return */ @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); // 使用springSecurity提供的密码加密匹配类 // return new MyPasswordEncoder(); // 使用自定义的密码加密匹配类 } }
2.3 自定义加密类
就是一个实现了 PasswordEncoder接口的类而已,我们可以通过该类来实现MD5加密或者一些其他的加密方式
2.3.2 加密类
用于实现自己的加密算法
package com.example.wiremock.config; import org.springframework.security.crypto.password.PasswordEncoder; /** * @author 王杨帅 * @create 2018-05-12 22:41 * @desc **/ public class MyPasswordEncoder implements PasswordEncoder { @Override public String encode(CharSequence charSequence) { return charSequence.toString(); } @Override public boolean matches(CharSequence charSequence, String s) { if (charSequence.toString().equals(s)) { return true; } return false; } }
2.3.3 重新进行SrpingSecurity配置
package com.xunyji.springsecurity01.config; //import com.xunyji.springsecurity01.service.FuryUserDetailService; import com.xunyji.springsecurity01.service.FuryUserDetailService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; /** * @author 王杨帅 * @create 2018-09-07 22:18 * @desc **/ @Configuration //@EnableWebSecurity public class SpringSecurityConfig extends WebSecurityConfigurerAdapter{ @Autowired private FuryUserDetailService furyUserDetailService; @Autowired private PasswordEncoder passwordEncoder; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { // 内存认证 start // auth.inMemoryAuthentication() // .passwordEncoder(new MyPasswordEncoder()) // .withUser("admin") // .password("123321") // .roles("ADMIN"); // auth.inMemoryAuthentication() // .passwordEncoder(new MyPasswordEncoder()) // .withUser("wys") // .password("123321") // .roles("USER"); // 内存认证 end auth.authenticationProvider(authenticationProvider()); // 添加自定义的认证逻辑 } /** * 创建认证提供者Bean * 技巧01:DaoAuthenticationProvider是SpringSecurity提供的AuthenticationProvider实现类 * @return */ @Bean public DaoAuthenticationProvider authenticationProvider() { DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); // 创建DaoAuthenticationProvider实例 authProvider.setUserDetailsService(furyUserDetailService); // 将自定义的认证逻辑添加到DaoAuthenticationProvider authProvider.setPasswordEncoder(passwordEncoder); // 设置自定义的密码加密 return authProvider; } @Override public void configure(WebSecurity web) throws Exception { super.configure(web); } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/test/home").permitAll() .anyRequest().authenticated() .and() .logout().permitAll() .and() .formLogin(); http.csrf().disable(); // 关闭csrf验证 } /** * 创建PasswordEncoder对应的Bean * @return */ @Bean public PasswordEncoder passwordEncoder() { // return new BCryptPasswordEncoder(); // 使用springSecurity提供的密码加密匹配类 return new MyPasswordEncoder(); // 使用自定义的密码加密匹配类 } }
2.4 测试
启动项目后进入到登录页面
技巧01:随便输入一个用户名(PS:由于后台没有实现根据用户名查询用户信息的逻辑,若依随便输入一个即可),输入一个固定的密码(PS:这个密码要和loadUserByUsername方法中返回的User对象中的password参数加密前的内容一致)
2.5 进阶
loadUserByUsername 方法的返回类型是一个User对象,这个User对象有两个默认的构造器,一个仅仅包含用户名、用户秘密和权限,另一个除了包含这些信息还包含一些用户的有效性信息
技巧01:直接看 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.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; import java.util.function.Function; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.security.core.CredentialsContainer; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.crypto.factory.PasswordEncoderFactories; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.util.Assert; public class User implements UserDetails, CredentialsContainer { private static final long serialVersionUID = 500L; private static final Log logger = LogFactory.getLog(User.class); private String password; private final String username; private final Set<GrantedAuthority> authorities; private final boolean accountNonExpired; private final boolean accountNonLocked; private final boolean credentialsNonExpired; private final boolean enabled; public User(String username, String password, Collection<? extends GrantedAuthority> authorities) { this(username, password, true, true, true, true, authorities); } public User(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) { if (username != null && !"".equals(username) && password != null) { this.username = username; this.password = password; this.enabled = enabled; this.accountNonExpired = accountNonExpired; this.credentialsNonExpired = credentialsNonExpired; this.accountNonLocked = accountNonLocked; this.authorities = Collections.unmodifiableSet(sortAuthorities(authorities)); } else { throw new IllegalArgumentException("Cannot pass null or empty values to constructor"); } } public Collection<GrantedAuthority> getAuthorities() { return this.authorities; } public String getPassword() { return this.password; } public String getUsername() { return this.username; } public boolean isEnabled() { return this.enabled; } public boolean isAccountNonExpired() { return this.accountNonExpired; } public boolean isAccountNonLocked() { return this.accountNonLocked; } public boolean isCredentialsNonExpired() { return this.credentialsNonExpired; } public void eraseCredentials() { this.password = null; } private static SortedSet<GrantedAuthority> sortAuthorities(Collection<? extends GrantedAuthority> authorities) { Assert.notNull(authorities, "Cannot pass a null GrantedAuthority collection"); SortedSet<GrantedAuthority> sortedAuthorities = new TreeSet(new User.AuthorityComparator()); Iterator var2 = authorities.iterator(); while(var2.hasNext()) { GrantedAuthority grantedAuthority = (GrantedAuthority)var2.next(); Assert.notNull(grantedAuthority, "GrantedAuthority list cannot contain any null elements"); sortedAuthorities.add(grantedAuthority); } return sortedAuthorities; } public boolean equals(Object rhs) { return rhs instanceof User ? this.username.equals(((User)rhs).username) : false; } public int hashCode() { return this.username.hashCode(); } public String toString() { StringBuilder sb = new StringBuilder(); sb.append(super.toString()).append(": "); sb.append("Username: ").append(this.username).append("; "); sb.append("Password: [PROTECTED]; "); sb.append("Enabled: ").append(this.enabled).append("; "); sb.append("AccountNonExpired: ").append(this.accountNonExpired).append("; "); sb.append("credentialsNonExpired: ").append(this.credentialsNonExpired).append("; "); sb.append("AccountNonLocked: ").append(this.accountNonLocked).append("; "); if (!this.authorities.isEmpty()) { sb.append("Granted Authorities: "); boolean first = true; Iterator var3 = this.authorities.iterator(); while(var3.hasNext()) { GrantedAuthority auth = (GrantedAuthority)var3.next(); if (!first) { sb.append(","); } first = false; sb.append(auth); } } else { sb.append("Not granted any authorities"); } return sb.toString(); } public static User.UserBuilder withUsername(String username) { return builder().username(username); } public static User.UserBuilder builder() { return new User.UserBuilder(); } /** @deprecated */ @Deprecated public static User.UserBuilder withDefaultPasswordEncoder() { logger.warn("User.withDefaultPasswordEncoder() is considered unsafe for production and is only intended for sample applications."); PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder(); User.UserBuilder var10000 = builder(); encoder.getClass(); return var10000.passwordEncoder(encoder::encode); } public static User.UserBuilder withUserDetails(UserDetails userDetails) { return withUsername(userDetails.getUsername()).password(userDetails.getPassword()).accountExpired(!userDetails.isAccountNonExpired()).accountLocked(!userDetails.isAccountNonLocked()).authorities(userDetails.getAuthorities()).credentialsExpired(!userDetails.isCredentialsNonExpired()).disabled(!userDetails.isEnabled()); } public static class UserBuilder { private String username; private String password; private List<GrantedAuthority> authorities; private boolean accountExpired; private boolean accountLocked; private boolean credentialsExpired; private boolean disabled; private Function<String, String> passwordEncoder; private UserBuilder() { this.passwordEncoder = (password) -> { return password; }; } public User.UserBuilder username(String username) { Assert.notNull(username, "username cannot be null"); this.username = username; return this; } public User.UserBuilder password(String password) { Assert.notNull(password, "password cannot be null"); this.password = password; return this; } public User.UserBuilder passwordEncoder(Function<String, String> encoder) { Assert.notNull(encoder, "encoder cannot be null"); this.passwordEncoder = encoder; return this; } public User.UserBuilder roles(String... roles) { List<GrantedAuthority> authorities = new ArrayList(roles.length); String[] var3 = roles; int var4 = roles.length; for(int var5 = 0; var5 < var4; ++var5) { String role = var3[var5]; Assert.isTrue(!role.startsWith("ROLE_"), role + " cannot start with ROLE_ (it is automatically added)"); authorities.add(new SimpleGrantedAuthority("ROLE_" + role)); } return this.authorities((Collection)authorities); } public User.UserBuilder authorities(GrantedAuthority... authorities) { return this.authorities((Collection)Arrays.asList(authorities)); } public User.UserBuilder authorities(Collection<? extends GrantedAuthority> authorities) { this.authorities = new ArrayList(authorities); return this; } public User.UserBuilder authorities(String... authorities) { return this.authorities((Collection)AuthorityUtils.createAuthorityList(authorities)); } public User.UserBuilder accountExpired(boolean accountExpired) { this.accountExpired = accountExpired; return this; } public User.UserBuilder accountLocked(boolean accountLocked) { this.accountLocked = accountLocked; return this; } public User.UserBuilder credentialsExpired(boolean credentialsExpired) { this.credentialsExpired = credentialsExpired; return this; } public User.UserBuilder disabled(boolean disabled) { this.disabled = disabled; return this; } public UserDetails build() { String encodedPassword = (String)this.passwordEncoder.apply(this.password); return new User(this.username, encodedPassword, !this.disabled, !this.accountExpired, !this.credentialsExpired, !this.accountLocked, this.authorities); } } private static class AuthorityComparator implements Comparator<GrantedAuthority>, Serializable { private static final long serialVersionUID = 500L; private AuthorityComparator() { } public int compare(GrantedAuthority g1, GrantedAuthority g2) { if (g2.getAuthority() == null) { return -1; } else { return g1.getAuthority() == null ? 1 : g1.getAuthority().compareTo(g2.getAuthority()); } } } }
// // 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<? extends GrantedAuthority> getAuthorities(); String getPassword(); String getUsername(); boolean isAccountNonExpired(); boolean isAccountNonLocked(); boolean isCredentialsNonExpired(); boolean isEnabled(); }
3 SpringSecurity配置详解
3.1 默认配置简述
默认的SpringSecurity会对所有都请求都进行权限验证,而且只包含user用户的用户信息,而在实际的开发中需要过滤掉某些请求(例如:登录请求);如果我们实现了UserDetailsService接口,那就必须进行SpringSecurity配置,因为实现了UserDetailsService接口后需要用到一个返回类型是PasswordEncoder的Bean,而这个Bean必须在SpringSecurity的配置文件中进行配置
3.2 配置详情
技巧01:在继承了WebSecurityConfigurerAdapter的子类中重写 configure 方法即可
坑01:configure() 方法有多个重载方法,我们需要重写参数类型是 HttpSecurity 那个重载方法
3.2.1 配置登录方式
技巧01:如果重写了 configure() 方法后之前默认的SpringSecurity配置就会失效(例如:对所有请求都进行权限验证),我们需要自定义对那些请求进行权限验证
登录方式有两种:
》表单登录方式
提交后会被 UsernamePasswordAuthenticationFilter 过滤
》弹出弹出窗口登录方式
登录后会被 BasicAuthenticationFilter 过滤
@Override protected void configure(HttpSecurity http) throws Exception { http.httpBasic() // 配置弹出框登录 .and().authorizeRequests() // 请求权限设置 .anyRequest() // 所有请求 .authenticated(); // 所有请求都进行权限验证 }
package com.example.wiremock.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 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.NoOpPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; @Configuration public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter { @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); // return new MyPasswordEncoder(); } @Override protected void configure(HttpSecurity http) throws Exception { http.httpBasic() // 配置弹出框登录 .and().authorizeRequests() // 请求权限设置 .anyRequest() // 所有请求 .authenticated(); // 所有请求都进行权限验证 } }
3.2.2 跨站防护
主要用于前后端分离时利用前端的登录页面进行模拟登录
package com.example.wiremock.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 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.NoOpPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; @Configuration public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter { @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); // return new MyPasswordEncoder(); } @Override protected void configure(HttpSecurity http) throws Exception { // http.httpBasic() // 配置弹出框登录 http.formLogin() .and().authorizeRequests() // 请求权限设置 .anyRequest() // 所有请求 .authenticated() // 所有请求都进行权限验证 .and().csrf().disable(); // 取消“跨站防护” } }
3.2.3 自定义登录页面和登录提交路径
3.2.3.1 自定义登录页面
创建一个名为 xiangxu-login.html 的HTML文件作为自定义的登录页面
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Insert title here</title> </head> <body> <h2>自定义表单登陆</h2> <form action="/login" method="post"> <table> <tr> <td>用户名:</td> <td> <input type="text" name="username" /> </td> </tr> <tr> <td>密码:</td> <td> <input type="password" name="password" /> </td> </tr> <tr> <td colspan="2"> <button type="submit">登陆</button> </td> </tr> </table> </form> </body> </html>
技巧01:表单请求字段必须包含username和password,表单提交方式为POST,默认的表单提交路径为 /login
技巧02:如果想要自定义的表单登录页面就需要进行SpringSecurity配置
技巧03:该HTML文件必须放在resources目录下的resources文件夹下,形如
3.2.3.2 自定义登录路径
修改自定义页面中表单的请求路径,默认是 /login,本案例修改成:/dev/xiangxu/login
3.2.3.3 配置SpringSecurity
如果想要让自定义的登录页面和自定义的登录请求路径生效就必须进行SpringSecurity配置
技巧01:配置 loginPage 时,如果项目配置了上下文路径就必须加上上下文及路径,否则就找不到放在resources目录下resources文件夹中的请求页面
技巧02:配置 loginProcessingUrl 时就是配置请求页面表单的登录路径,表单怎么写的,这里的配置就怎么写
坑01:虽然都对自定义的登录页面以及登录请求进行了配置,但是会出现反复重定向的问题;因为我们的SrpingSecurity配置是会对所有的请求路径都进行权限校验的,所以我们必须排除掉对 登录请求页面 和 登录请求路径 的权限验证
3.2.4 登录成功后响应信息
默认的SpringSecurity配置在登录成功后就会跳转到之前的请求,登录失败就会继续跳转到登录页面
需求:基于RestfulAPI的前后端分离项目需要登录成功后直接返回JSON格式的响应即可,具体怎么跳转由前端去控制
技巧01:响应自定义登录成功和登录失败的响应格式只需要实现两个接口并进行SpringSecurity配置即可
》AuthenticationSuccessHandler
AuthenticationSuccessHandler是登录成功的接口
package com.example.wiremock.authentication; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; 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 com.fasterxml.jackson.databind.ObjectMapper; @Component public class XiangXuAuthenticationSuccessHandler implements AuthenticationSuccessHandler { private Logger log = LoggerFactory.getLogger(getClass()); @Autowired private ObjectMapper objectMapper; @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { // TODO Auto-generated method stub log.info("登陆成功"); response.setContentType("application/json;charset=UTF-8"); // 响应类型 response.getWriter().write(objectMapper.writeValueAsString(authentication)); // 数据转化成json类型后再进行响应操作 } }
》AuthenticationFailureHandler
AuthenticationFailureHandler是登录失败的接口
package com.example.wiremock.authentication; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.stereotype.Component; import com.fasterxml.jackson.databind.ObjectMapper; @Component public class XiangXuAuthenticationFailureHandler implements AuthenticationFailureHandler { private Logger log = LoggerFactory.getLogger(getClass()); @Autowired private ObjectMapper ObjectMapper; @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { log.info("登陆失败"); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write(ObjectMapper.writeValueAsString(exception));; } }
》SpringSecurity配置
package com.example.wiremock.config; import com.example.wiremock.authentication.XiangXuAuthenticationFailureHandler; import com.example.wiremock.authentication.XiangXuAuthenticationSuccessHandler; import com.example.wiremock.entity.properties.SecurityProperty; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; 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; import javax.annotation.PostConstruct; import javax.annotation.Resource; import java.net.SecureCacheResponse; @Configuration @Slf4j public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private SecurityProperty securityProperty; @Autowired private XiangXuAuthenticationSuccessHandler xiangXuAuthenticationSuccessHandler; @Autowired private XiangXuAuthenticationFailureHandler xiangXuAuthenticationFailureHandler; private String LOGIN_PAGE; @PostConstruct public void init() { LOGIN_PAGE = securityProperty.getBrowser().getLoginPage(); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); // return new MyPasswordEncoder(); } @Override protected void configure(HttpSecurity http) throws Exception { // http.httpBasic() // 配置弹出框登录 http.formLogin() // .loginPage("/dev/xiangxu-login.html") .loginPage("/dev/authentication/require") // 自定义登陆页面 // .loginPage(LOGIN_PAGE) .loginProcessingUrl("/dev/xiangxu/login") // 自定义登陆请求路径(默认是:/login) .successHandler(xiangXuAuthenticationSuccessHandler) // 自定义成功处理器 .failureHandler(xiangXuAuthenticationFailureHandler) // 自定义失败处理器 .and().authorizeRequests() // 请求权限设置 // .antMatchers("/dev/xiangxu-login.html").permitAll() .antMatchers("/dev/authentication/require").permitAll() .antMatchers(LOGIN_PAGE).permitAll() .antMatchers("/dev/xiangxu/login").permitAll() .anyRequest() // 所有请求 .authenticated() // 所有请求都进行权限验证 .and().csrf().disable(); // 取消“跨站防护” } }
4 SpringSecurity配置进阶
4.1 登录页面个性化配置
需求:开发者仅仅需要在properties文件中按照规定的格式配置登录页面即可,例如
xiangxu.security.browser.loginPage = /dev/xiangxu-login.html
4.1.1 properties文件配置
在properties文件中配置登录页面
#security.basic.enabled = false
xiangxu.security.browser.loginPage = /dev/xiangxu-login.html
4.1.2 创建实体类
根据properties文件的配置格式创建实体类,参考博文 -> 点击前往
》SecurityProperty 实体类
技巧01:为 browser 属性设置初始值,目的是为了依赖注入后在使用依赖注入对象时出现空指针异常
package com.example.wiremock.entity.properties; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; /** * @author 王杨帅 * @create 2018-05-13 13:45 * @desc **/ @ConfigurationProperties(prefix = "xiangxu.security") public class SecurityProperty { private BrowserProperty browser = new BrowserProperty(); public BrowserProperty getBrowser() { return browser; } public void setBrowser(BrowserProperty browser) { this.browser = browser; } }
》BrowserProperty 实体类
技巧01:为 loginPage 属性设置初始值,目的是为了在 properties 文件中不进行登录页面配置时使用默认指定的登录页面(PS:不是SpringSecurity指定的表单登录页面,而是我们自己开发的登录页面)
package com.example.wiremock.entity.properties; /** * @author 王杨帅 * @create 2018-05-13 13:46 * @desc **/ public class BrowserProperty { private String loginPage = "/login.html"; public String getLoginPage() { return loginPage; } public void setLoginPage(String loginPage) { this.loginPage = loginPage; } }
》实体类配置类
package com.example.wiremock.config; import com.example.wiremock.entity.properties.SecurityProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Configuration; @Configuration @EnableConfigurationProperties(SecurityProperty.class) public class PropertiesConfig { }
4.1.3 SpringSecurity配置
》在配置类中依赖注入SecurityProperty
》定义一个LOGIN_PAGE属性来存放登录页面路径
》初始化LOGIN_PAGE
技巧01:不能利用依赖注入的对象去初始化成员变量,解决办法是创建一个初始化方法,在这个初始化方法中利用依赖注入的去初始化成员变量,但是这个初始化方法必须添加@PostConstruct 注解;@PostConstruct注解参考博文 -> 点击前往
》指定登录页面、登录表单请求url
》排除登录页面、登录表单请求url的权限验证
package com.example.wiremock.config; import com.example.wiremock.entity.properties.SecurityProperty; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; 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; import javax.annotation.PostConstruct; import javax.annotation.Resource; import java.net.SecureCacheResponse; @Configuration @Slf4j public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter { @Resource private SecurityProperty securityProperty; private String LOGIN_PAGE; @PostConstruct public void init() { LOGIN_PAGE = securityProperty.getBrowser().getLoginPage(); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); // return new MyPasswordEncoder(); } @Override protected void configure(HttpSecurity http) throws Exception { // http.httpBasic() // 配置弹出框登录 http.formLogin() // .loginPage("/dev/xiangxu-login.html") // .loginPage("/dev/authentication/require") // 自定义登陆页面 .loginPage(LOGIN_PAGE) .loginProcessingUrl("/dev/xiangxu/login") // 自定义登陆请求路径(默认是:/login) .and().authorizeRequests() // 请求权限设置 // .antMatchers("/dev/xiangxu-login.html").permitAll() // .antMatchers("/dev/authentication/require").permitAll() .antMatchers(LOGIN_PAGE).permitAll() .antMatchers("/dev/xiangxu/login").permitAll() .anyRequest() // 所有请求 .authenticated() // 所有请求都进行权限验证 .and().csrf().disable(); // 取消“跨站防护” } }
5 登录页面处理
需求:根据请求url的类型进行不同的登录响应,例如 -> 如果请求url是以 .html 结尾的就直接返回登录页面,如果不是就直接返回JSON格式的提示信息(PS:假设请求url还未进行权限验证)
5.1 思路
在SpingSecurity的配置类中修改登录页面,将登录页面修改成一个congroller层控制方法;在该控制方法中来判断请求路径的类型,然后做出不同的响应
5.2 开发步骤
5.2.1 修改SpringSecurity配置
将登录页面修改为一个控制层的请求路径
技巧01:任然要排除登录页面的权限控制,因为最终会跳转到登录页面进行登录
package com.example.wiremock.config; import com.example.wiremock.entity.properties.SecurityProperty; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; 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; import javax.annotation.PostConstruct; import javax.annotation.Resource; import java.net.SecureCacheResponse; @Configuration @Slf4j public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter { @Resource private SecurityProperty securityProperty; private String LOGIN_PAGE; @PostConstruct public void init() { LOGIN_PAGE = securityProperty.getBrowser().getLoginPage(); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); // return new MyPasswordEncoder(); } @Override protected void configure(HttpSecurity http) throws Exception { // http.httpBasic() // 配置弹出框登录 http.formLogin() // .loginPage("/dev/xiangxu-login.html") .loginPage("/dev/authentication/require") // 自定义登陆页面 // .loginPage(LOGIN_PAGE) .loginProcessingUrl("/dev/xiangxu/login") // 自定义登陆请求路径(默认是:/login) .and().authorizeRequests() // 请求权限设置 // .antMatchers("/dev/xiangxu-login.html").permitAll() .antMatchers("/dev/authentication/require").permitAll() .antMatchers(LOGIN_PAGE).permitAll() .antMatchers("/dev/xiangxu/login").permitAll() .anyRequest() // 所有请求 .authenticated() // 所有请求都进行权限验证 .and().csrf().disable(); // 取消“跨站防护” } }
5.2.2 编写控制层逻辑
》获取请求url信息
从请求缓存中获取用户的请求信息
》根据从请求缓存中获取到的请求来判断响应内容
如果请求url是以 .html 结尾就返回登录页面,反之返回JSON格式的提示信息
package com.example.wiremock.controller; import java.io.IOException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import com.example.wiremock.entity.SimpleResponse; import com.example.wiremock.entity.properties.SecurityProperty; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.security.web.DefaultRedirectStrategy; import org.springframework.security.web.RedirectStrategy; import org.springframework.security.web.savedrequest.HttpSessionRequestCache; import org.springframework.security.web.savedrequest.RequestCache; import org.springframework.security.web.savedrequest.SavedRequest; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; @RestController public class BrowserSecurityController { private Logger log = LoggerFactory.getLogger(getClass()); /** 请求缓存对象 */ private RequestCache requestCache = new HttpSessionRequestCache(); // 请求缓存对象,利用该对象可以获取一些请求的缓存 /** 重定向对象 */ private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy(); // 页面跳转用的 /** 依赖注入 登录页面配置信息实体类 */ @Autowired private SecurityProperty securityProperty; /** * 所有进行身份验证的请求都会被跳转到这里 * @param request * @param response * @return * @throws IOException */ @RequestMapping(value = "/authentication/require") @ResponseStatus(code = HttpStatus.UNAUTHORIZED) public SimpleResponse requireAuthentication(HttpServletRequest request, HttpServletResponse response) throws IOException { SavedRequest savedRequest = requestCache.getRequest(request, response); if (savedRequest != null) { // 获取请求缓存成功时 String targetUrl = savedRequest.getRedirectUrl(); // 获取请求缓存的url log.info("引发请求的url为:" + targetUrl); if (StringUtils.endsWithIgnoreCase(targetUrl, ".html")) { // 如果缓存请求的url是以.html结尾就进行跳转 log.info("=== ONE ==="); log.info("配置文件中的登陆页路径为:" + securityProperty.getBrowser().getLoginPage()); log.info("=== TWO ==="); redirectStrategy.sendRedirect(request, response, securityProperty.getBrowser().getLoginPage()); } } return new SimpleResponse("访问的服务需要进行身份验证,请引导用户进行到登录页面。"); } }
5.2.3 测试
》非 .html 结尾的请求
》.html 结尾的请求
只要是 .html 结尾的都行
技巧01:请求发出后会自动跳转到登录页面
bug01:跳转登录页面进行登录操作,登录成功后就会跳转到之前的请求中;我们现在的项目一般都是基于RestfulAPI的前后端分离项目,我们要求权限验证成功后直接返回一个JSON格式的信息就行啦,该问题待解决:详情参见3.2.4 登录成功后响应信息
6 响应格式处理
SpringSecurity默认登录成功后会跳转到之前的请求路径,登录失败后跳转到 /error;开发者可以将定义响应的数据格式为JSON格式
需求:根据用户在properties文件中的配置来决定登录后的响应格式
技巧01:以JSON格式响应 -> 请参见3.2.4 登录成功后响应信息
6.1 创建响应类型枚举
该枚举主要用来指定登录后的响应数据类型
package com.example.wiremock.enums; /** * @author 王杨帅 * @create 2018-05-13 17:23 * @desc **/ public enum LoginType { REDIRECT, JSON }
6.2 Properties自定义配置实体类
技巧01:给BrowserProperty的loginType属性设置了默认值,用来指明默认的登录后响应类型是JSON类型
package com.example.wiremock.entity.properties; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; /** * @author 王杨帅 * @create 2018-05-13 13:45 * @desc **/ @ConfigurationProperties(prefix = "xiangxu.security") public class SecurityProperty { private BrowserProperty browser = new BrowserProperty(); public BrowserProperty getBrowser() { return browser; } public void setBrowser(BrowserProperty browser) { this.browser = browser; } }
package com.example.wiremock.entity.properties; import com.example.wiremock.enums.LoginType; /** * @author 王杨帅 * @create 2018-05-13 13:46 * @desc **/ public class BrowserProperty { private String loginPage = "/dev/xiangxu-login.html"; private LoginType loginType = LoginType.JSON; public String getLoginPage() { return loginPage; } public void setLoginPage(String loginPage) { this.loginPage = loginPage; } public LoginType getLoginType() { return loginType; } public void setLoginType(LoginType loginType) { this.loginType = loginType; } }
6.3 重写登录成功后的两个处理类
6.3.1 XiangXuAuthenticationSuccessHandler
不在是实现AuthenticationSuccessHandler接口,而是继承SavedRequestAwareAuthenticationSuccessHandler父类
package com.example.wiremock.authentication; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import com.example.wiremock.entity.properties.SecurityProperty; import com.example.wiremock.enums.LoginType; 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.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler; import org.springframework.stereotype.Component; import com.fasterxml.jackson.databind.ObjectMapper; @Component //public class XiangXuAuthenticationSuccessHandler implements AuthenticationSuccessHandler { public class XiangXuAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler { private Logger log = LoggerFactory.getLogger(getClass()); /** json数据转化对象 */ @Autowired private ObjectMapper objectMapper; /** 自定义配置类对象 */ @Autowired private SecurityProperty securityProperty; @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { log.info("登陆成功"); // 登录后的形影数据格式判断 if (LoginType.JSON.equals(securityProperty.getBrowser().getLoginType())) { response.setContentType("application/json;charset=UTF-8"); // 响应类型 response.getWriter().write(objectMapper.writeValueAsString(authentication)); // 数据转化成json类型后再进行响应操作 } else { super.onAuthenticationSuccess(request, response, authentication); } } }
6.3.2 XiangXuAuthenticationFailureHandler
不在是实现AuthenticationFailureHandler接口,而是继承SimpleUrlAuthenticationFailureHandler父类
package com.example.wiremock.authentication; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import com.example.wiremock.entity.properties.SecurityProperty; import com.example.wiremock.enums.LoginType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; import org.springframework.stereotype.Component; import com.fasterxml.jackson.databind.ObjectMapper; @Component //public class XiangXuAuthenticationFailureHandler implements AuthenticationFailureHandler { public class XiangXuAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler { private Logger log = LoggerFactory.getLogger(getClass()); @Autowired private ObjectMapper ObjectMapper; /** 自定义配置类对象 */ @Autowired private SecurityProperty securityProperty; @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { log.info("登陆失败"); // 登录后响应数据格式判断 if (LoginType.JSON.equals(securityProperty.getBrowser().getLoginType())) { // JSON格式返回 response.setContentType("application/json;charset=UTF-8"); response.getWriter().write(ObjectMapper.writeValueAsString(exception));; } else { // HTML跳转 super.onAuthenticationFailure(request, response, exception); } } }
6.3.3 properties配置
#security.basic.enabled = false
xiangxu.security.browser.loginPage = /dev/xiangxu-login.html
xiangxu.security.browser.loginType = REDIRECT
https://cloud.tencent.com/developer/support-plan?invite_code=xtjeour7cozu
·下面是我的公众号二维码,欢迎关注·
尋渝記
微信号:xyj_fury