目标
表单输入用户名密码,结合数据库方式进行身份认证和权限鉴定。通过这样一个过程来熟悉 Spring Security 的认证流程,掌握 Spring Security 的原理。
回顾 Spring Security(一)基本介绍
https://blog.csdn.net/qq_19636353/article/details/126964919
Spring Security 执行登录认证过程
自定义用户名密码认证
1.自定义配置类
创建自定义的Java配置类WebSecurityConfig.java,继承WebSecurityConfigureAdapter抽象类,使用@Configuration和 @EnableWebSecurity进行注解声明配置。实现configure(HttpSecurity http)方法,设置一些跟网页请求相关的设置。
该配置类继承抽象类 WebSecurityConfigurerAdapter,重写内部的方法就可以了,分别是对AuthenticationManagerBuilder,WebSecurity,HttpSecurity方法,最终实现自定义认证。
在设置参数的时候可以使用链式编程,在后面添加 . 就能不断的进行配置,我们用http下面的formLogin()下面的方法来设置我们的登录页。
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable(); //关闭csrf,默认是开启的,开启后会导致我们的登录被拦截,导致登录失败
http.formLogin()
.loginPage("/toLogin") //设置默认登录页
.usernameParameter("username") //设置从前端提交过来的用户名参数
.passwordParameter("password") //设置从前端提交过来的密码参数
.defaultSuccessUrl("/index") //设置默认登录页面,会跳转到登录前的路径
// .successForwardUrl("/index") //登录成功后页面,无论前面为什么请求,都会跳转到这个路径
.failureForwardUrl("/toLogin") //设置登录失败后的请求
.loginProcessingUrl("/login"); //设置登录请求路径
}
}
配置解析
@EnableWebSecurity 查看其注解源码,主要是引用WebSecurityConfiguration.class 和 加入了@EnableGlobalAuthentication 注解,这里只要明白添加 @EnableWebSecurity 注解将开启 Security 功能。
1. formLogin() 使用表单登录(默认请求地址为 /login),在SpringSecurity5 里其实已经将旧版本默认的 httpBasic() 更换成 formLogin()了,这里为了表明表单登录还是配置了一次。
2. authorizeRequests() 开始请求权限配置
3. antMatchers() 使用Ant风格的路径匹配,这里配置匹配 / 和 /index
4. permitAll() 用户可任意访问
5. anyRequest() 匹配所有路径
6. authenticated() 用户登录后可访问
无Session状态
用户授权
在网页中有些特定的页面只能有对应的权限才能访问。
启动项目可以访问index.html,所有人都可以访问,但是admin和user的测试页面在没有登录的情况下无法访问,会直接跳转到登录页面。
在之前的自定义登录页面前加入如下代码
//antMatchers括号里面可以输入多个请求路径,用逗号隔开
http.authorizeRequests() //设置访问请求的权限
.antMatchers("/") //默认首页允许所有用户都可以访问,不登录也能访问
.permitAll()
.antMatchers("/admin/*") //admin下面的页面只有拥有admin角色的用户才能访问
.hasRole("admin")
.antMatchers("/user/*") //user下面的页面只有拥有user角色的用户才能访问
.hasRole("user");
4. 用户认证
用户认证是用户在登录的时候验证用户的用户名和密码并赋予他们相应的权限,从数据库中查看全部用户的信息,然后将所有的用户信息进行认证,然后登录用户就能获得相应的权限去访问相应的页面。
InMemoryUserDetailsManager:顾名思义,将用户名密码存储在内存中的用户管理器,可设定多个用户。
如果正常开发需要连接数据库,通过数据库查找到用户的信息进行认证并赋予权限。
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//使用BCryptPasswordEncoder对密码进行加密
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
.withUser("admin") //给admin用户添加admin权限
.password(new BCryptPasswordEncoder().encode("123456")).roles("admin","user")
.and()
.withUser("user") //给user用户添加user权限
.password(new BCryptPasswordEncoder().encode("123456")).roles("user");
放行静态资源
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/css/**");
}
登录user
user访问admin接口,返回403提示无权限
配置资源认证规则 DaoAuthenticationProvider
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{
@Autowired
private UserDetailsService userDetailsService;
/**
* 配置用户认证, 使用自定义身份验证组件
*/
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
// auth.userDetailsService(userDetailsService);
//创建DaoAuthenticationProvider
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
//设置userDetailsService,基于数据库方式进行身份认证
provider.setUserDetailsService(userDetailsService);
provider.setPasswordEncoder(passwordEncoder()); //配置密码编码器
auth.authenticationProvider(provider);
}
@Override
public void configure(HttpSecurity http)throws Exception{
http.formLogin(); //输入表单用户名密码
http.rememberMe().and()
.cors().and().csrf().disable()
.authorizeRequests()
.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
//.antMatchers("/").permitAll()
.antMatchers("/admin/**")..hasRole("admin")
//.antMatchers("/user/**")..hasRole("user")
.anyRequest().authenticated();// 其他所有请求需要身份认证
}}
@Bean
public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }
实现 UserDetailsService
UserDetailsService是Spring Security中的一个接口,它用于从特定数据源(如数据库)中获取用户详细信息,以进行身份验证和授权。实现该接口的类需要实现loadUserByUsername()方法,该方法根据给定的用户名返回一个UserDetails对象,该对象包含有关用户的详细信息,例如密码、角色和权限等。在Spring Security中,UserDetailsService通常与DaoAuthenticationProvider一起使用,后者是一个身份验证提供程序,用于验证用户的凭据。
想要关闭UserDetailsService的自动配置,则添加一个自定义实现类UserDetailsServiceImpl,改变认证的用户信息来源,实现 UserDetailsService,重写loadUserByUsername()方法。
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.Service;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private LoginDao loginDao;
@Autowired
private PermissionDao permissionDao;
//@PostConstruct
public void initData(){
String password = passwordEncoder.encode("123456");
userList.add(new User("admin", password, AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_admin","ROLE_user")));
userList.add(new User("user", password, AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_user")));
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = loginDao.loadUserByUsername(username);
if(user == null) {
throw new UsernameNotFoundException("用户名不存在!" + username);
}
user.setPermissions(permissionDao.findPermissionsByUserId(user.id));
System.out.println("权限列表:" + user.getPermissions());
return user;
}}
Controller 测试类
@RestController
public class UserController{
@GetMapping("/user/getCurrentUser")
public Object getCurrentUser(Authentication authentication){
log.info("getCurrentUser ok");
return authentication;
}
//测试类
@RequestMapping(value = "/test")
public String test() {
return "security test success";
}
}
测试接口
输入用户名admin,密码 123456
访问接口 http://localhost:9507/user/getCurrentUser
请求结果
UsernamePasswordAuthenticationToken [Principal=org.springframework.security.core.userdetails.User [Username=admin, Password=[PROTECTED], Enabled=true, AccountNonExpired=true, credentialsNonExpired=true, AccountNonLocked=true, Granted Authorities=[admin]], Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=974646B19094BBB5A624D56061E7D507], Granted Authorities=[admin]]
其他详细说明
http.formLogin()
自定义HttSecurity过滤器链,就自动关闭了其他默认的一些功能。
loginProcessingUrl
loginProcessingUrl(“/login/doLogin”) 是表单提交路劲,提交后执行认证逻辑。
loginProcessingUrl作用是用来拦截前端页面对 /login/doLogin 这个的请求的,拦截到了就走它自己的处理流程,导致我们自己后端写的/login/doLogin这个接口有写跟没写是一样的。
如果只配置loginPage而不配置loginProcessingUrl的话,那么loginProcessingUrl默认就是loginPage
你配置的loginPage("/testpage.html") ,那么loginProcessingUrl就是"/testpage.html"
优先级从上向下:
.successHandler(authenticationSuccess)
.successHandler(new ForwardAuthenticationSuccessHandler("/index2"))
.defaultSuccessUrl("/index1")
AuthenticationSuccessHandler
登录成功之后的处理逻辑
(重点) 由于未在WebSecurity配置中设置successHandler,因此默认情况下将调用SavedRequestAwareAuthenticationSuccessHandler。
ExceptionTranslationFilter在认证开始前把request缓存到session中,当认证成功后,在SavedRequestAwareAuthenticationSuccessHandler里取出缓存的request,跳转回认证前用户想访问的url
禁用session的操作
在configure方法,禁用session后,不会存储前一次请求信息,也就是跳转路径为/
//禁用session
httpSecurity.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
重定向到认证前URL
ExceptionTranslationFilter在认证开始前把request缓存到session中,当认证成功后,在SavedRequestAwareAuthenticationSuccessHandler里取出缓存的request,跳转回认证前用户想访问的URL。
在spring security中RequestCache有三个默认实现,HttpSessionRequestCache通过session存储前一次请求信息、NullRequestCache一般禁用session的情况下使用,不会存储前一次请求信息、CookieRequestCache使用cookie存储前一次请求信息。
链接:https://blog.youkuaiyun.com/fggdgh/article/details/125025287
自定义异常处理
SpringSecurity(二十):异常处理 https://www.cnblogs.com/wangstudyblog/p/14823512.html
http.exceptionHandling().authenticationEntryPoint()
http.exceptionHandling().authenticationEntryPoint(new CustomAuthenticationExceptionEntryPoint())
PasswordEncoder 自定义编码器
MyPasswordEncoder.java
public class MyPasswordEncoder implements PasswordEncoder {
@Override
public String encode(CharSequence charSequence) {
return charSequence.toString();
}
@Override
public boolean matches(CharSequence charSequence, String s) {
return s.equals(charSequence.toString());
}
}
原理解析
```csharp
Spring Security 原理解析【3】——认证与授权
https://www.cnblogs.com/xfeiyun/p/16214661.html
Spring SecurityFilterChain 过滤器链
SpringSecurity的核心是一个过滤器链,所有的请求都会经过这些过滤器,通过拦截url从而实现权限的控制。每个过滤器都有特定的职责,可通过配置添加、删除过滤器。过滤器的排序很重要,因为它们之间有依赖关系。有些过滤器也不能删除,如处在过滤器链最后几环的ExceptionTranslationFilter(处理后者抛出的异常),FilterSecurityInterceptor(最后一环,根据配置决定请求能不能访问服务)。
比较重要的三条过滤器:
- FilterSecurityInterceptor:是一个方法级的权限过滤器,基本位于过滤器链的最底部。
- ExceptionTranslationFilter:是个异常过滤器,用来处理在认证授权过程中抛出的异常。
- UsernamePasswordAuthenticationFilter:对/login的post请求做拦截,校验表单中的用户名和密码。
过滤器加载流程
使用SpringSecurity配置过滤器(DelegatingFilterProxy),在doFilter方法里使用初始化方法,得到一个叫FilterChainProxy的过滤器,在FilterChainProxy过滤器中得到所有的过滤器并加载到过滤链中,在SecurityFilterChain过滤器中调用getFilters方法完成加载。
实现Security需要用到两个比较重要的接口:
1、UserDetailsService接口:什么都没有配置的时候,登录账号和密码是由Sercurity定义生成,但当我们需要实际的账号和密码时,就需要通过自定义逻辑控制认证逻辑。
2、PasswordEncoder接口:数据加密接口,其中BCryptPasswordEncoder是Security官方推荐的密码解析器。表示验证从存储中获取的编码密码与编码后提交的原始密码是否匹配,如果密码匹配则返回 true。
认证流程 图
1. AbstractAuthenticationProcessingFilter 过滤器
AbstractAuthenticationProcessingFilter 过滤器是一个模版,主要是用来识别是否是认证请求,然后将不是认证请求到给到下一个过滤器的实现类(就是一组过滤器的串联)。这个过滤器中主要是doFilter方法,这个方法中requiresAuthentication 识别是否是认证请求,如果是,那么交给 attemptAuthentication 这个方法去处理,但是attemptAuthentication方法是抽象方法,需要子类去实现,即UsernamePasswordAuthenticationFilter 过滤器,也就是attemptAuthentication方法的具体实现类。
2. UsernamePasswordAuthenticationFilter 过滤器
处理认证请求的过滤器,是AbstractAuthenticationProcessingFilter的子类,真正实现类attemptAuthentication方法。
attemptAuthentication方法中,除了进行对请求中用户名、密码参数的处理外,核心的一行是this.getAuthenticationManager().authenticate(authRequest); 这个就是用选择合适的验证类来进行验证,从名称我们也能看出AuthenticationManager 是一个管理类,这个里面有多个AuthenticationProvider 对象。选择合适的验证就行。
但是这个getAuthenticationManager是一个接口,需要找到真正的实现类才行。
3. ProviderManager 管理类
这个类就是真正选择具体的认证类,authenticate方法遍历认证类,然后将请求中的用户名、密码进行验证的。
从上图可以看到,它就是对所有的认证类遍历,选择合适的进行。
provider.authenticate(authentication); 就是它的方法的核心代码。
但是AuthenticationProvider 也是一个接口,需要找其真正的实现类。
图
然后找到之后,在去调用additionalAuthenticationChecks 方法去验证持久化中的(内存或者retrieveUser方法找到的用户名和密码)是否和请求中的用户名密码一致,如果是一致的,那么就调用additionalAuthenticationChecks 方法去验证。
但是retrieveUser 和 additionalAuthenticationChecks 方法都是抽象方法,具体的实现,是子类进行的。
5. DaoAuthenticationProvider(子类)
这个是AbstractUserDetailsAuthenticationProvider 的子类,实现了retrieveUser(检索用户)和 additionalAuthenticationChecks (附加身份验证检查)方法,我们可以看到两个方法。
注意看 this.getUserDetailsService().loadUserByUsername(username); 这个方法就是我们自定义获取真正的(也就是持久化中)用户名 和 密码 时需要重写的方法,返回的user 类,就是UserDetails 的子类。
而且,additionalAuthenticationChecks 这个方法,就是调用PasswordEncoder 类中的matches 方法去对比的
。
至此,我们就将 SpringSecurity 用户登录验证流程走通了。
认证流程图
官网定义的关键过滤器顺序
HttpSecurity常用参数
HttpSecurity常用参数,如下图用法基本脱离不了下面这些方法,可以基于认证的方式有formLogin、openidLogin、oauth2Login,还可以做一些记住账号操作rememberMe,还可以进行session配置管理,还支持登出loginout等。
参考资料
Spring Security中文文档
https://www.springcloud.cc/spring-security.html#overall-architecture
Spring Boot2.0使用Spring Security
https://www.cnblogs.com/wtzbk/p/9387859.html
使用SpringBoot整合SpringSecurity
https://blog.csdn.net/qq_59570311/article/details/123157577
基于SpringBoot搭建应用开发框架(二) —— 登录认证
https://www.cnblogs.com/chiangchou/p/springboot-2.html#_label2_2
Spring Security 原理解析【3】——认证与授权
https://www.cnblogs.com/xfeiyun/p/16214661.html
SpringSecurity的认证流程分析(各个组件之间的关联)
https://blog.csdn.net/qq_35363507/article/details/121291755
SpringBoot2.x整合Security5(完美解决 There is no PasswordEncoder mapped for the id "null")
https://blog.csdn.net/SWPU_Lipan/article/details/80586054
https://blog.csdn.net/qq_21963133/article/details/81066714
springSecurity安全框架的学习和原理解读
https://blog.csdn.net/liushangzaibeijing/article/details/81220610
草稿
AccessDecisionManager
AccessDecisionManager:为 Web 或方法的安全提供访问决策。会注册一个默认的,但是我们也可以通过普通 bean 注册的方式使用自定义的 AccessDecisionManager。
SimpleUrlAuthenticationSuccessHandler.class
LoginAuthenticationSuccess.java
@Component
public class LoginAuthenticationSuccess extends SimpleUrlAuthenticationSuccessHandler {
@Autowired
private ObjectMapper objectMapper;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException
{
System.out.println("---Authentication Success(登录成功): " + authentication);
System.out.println("---getPrincipal: " + authentication.getPrincipal());
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(authentication));
}
}
限制最大登录数
config()中添加
.sessionManagement()
.invalidSessionUrl("/login/invalid")
.maximumSessions(1)
// 当达到最大值时,是否保留已经登录的用户
.maxSessionsPreventsLogin(false)
// 当达到最大值时,旧用户被踢出后的操作
.expiredSessionStrategy(new CustomExpiredSessionStrategy())
其他
关闭CSRF跨域
关闭CSRF跨域 CSRF(Cross-site request forgery)跨站请求伪造,也被称为one-click attack单键攻击;
草稿
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private LoginAuthenticationSuccess authenticationSuccess;//验证成功的处理类
@Bean
protected UserDetailsService userDetailsService() {
return new UserDetailsServiceImpl();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService()).passwordEncoder(new BCryptPasswordEncoder());
}
@Override
public void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests() //其他地址的访问均需验证权限
.anyRequest()
.authenticated()
.and()
.formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/loginSub")
//.defaultSuccessUrl("/")
.successHandler(authenticationSuccess)
.failureUrl("/failure.html")
.permitAll()
.and()
.logout()
.logoutUrl("/logout")
.permitAll();
http.csrf().disable();
//
//http.sessionManagement().invalidSessionUrl("/session/invalid"); //session失效时间
//http.exceptionHandling().accessDeniedHandler(accessDeniedHandler); //无权访问处理器
}
@Override
public void configure(WebSecurity web) throws Exception {
System.out.println("configure(WebSecurity web)...");
//解决静态资源被拦截的问题
web.ignoring().antMatchers("/static/**");
web.ignoring().antMatchers("/css/**");
web.ignoring().antMatchers("/js/**");
web.ignoring().antMatchers("/images/**");
web.ignoring().antMatchers("/**/favicon.ico");
web.ignoring().antMatchers("/lib/**");
web.ignoring().antMatchers("/fonts/**");
web.ignoring().antMatchers("/lang/**");
web.ignoring().antMatchers("/login.html");
//解决服务注册url被拦截的问题
web.ignoring().antMatchers("/**/*.json");
}
}