SpringSecurity到底是什么?
- 1.Security从字面上意思我就能看出它是一个安全框架,提供给我们自主验证的一套流程。
流程分析:
UsernamePasswordAuthenticationFilter未验证—AuthticationManager—AuthenticationProvider—UserDetailsService—UserDetails—Authentication(已认证)—SecurityContext—SecurityContextHolder—SecurityContextPersistenceFilter。
在其中在执行的工程中,实际上是由很多Filter组成的队列。
由于我账号不是vip不能画流程图,这里就不展示图给大家看了,在我的公众号里面有。
1.首先它和大多数框架一样需要配置(例如Mybatis),在这里,我们需要一个自定义的配置文件,当然,你如果不进行配置的话,程序在启动的时候,会走自己的默认路径,一但请求,直接被拦截。我们的配置文件必须要实现WebSecurityConfigurerAdapter接口,这个接口给我们定义了许多的认证方法,配置入下。
在项目中还需要添加我们的依赖包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version> 2.1.4.RELEASE</version>
</dependency>
- 配置文件,一定要继承WebSecurityConfigurerAdapter
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
*@Description:ContentUpdate authenticationManagerBuilder 添加用戶----角色
*@Date:2019/7/22
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.passwordEncoder(new BCryptPasswordEncoder())
.withUser("admin")
.password(new BCryptPasswordEncoder().encode("admin")).roles("ADMIN","USER")
.and()
.passwordEncoder(new BCryptPasswordEncoder())
.withUser("user")
.password(new BCryptPasswordEncoder().encode("user")).roles("USER");
}
/**
*@Description:ContentUpdate httpSecurity 添加角色----访问路径
*@Date:2019/7/22
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.mvcMatchers("/hello/**").hasRole("ADMIN")
.mvcMatchers("/other/**").hasRole("USER")
.mvcMatchers("/order/**").permitAll()
.mvcMatchers("/user/**").permitAll()
.mvcMatchers("/querInfo/**").permitAll()
.anyRequest()
.authenticated()
.and()
.formLogin()
.and()
.httpBasic();
}
}
- 然后我们写一个Controller进行测试一下
@RestController
@RequestMapping("/hello")
public class SecurityDemo {
@GetMapping("/security")
public String loginSecurity(){
return "Hello World!";
}
}
- 3.然后启动项目项目,输入请求路径,然后Security框架自动给你生成一个登陆页面,输入用户名admin,密码admin就可以看到打印结果Hello World!了,现在你已经初步认识了这个安全框架。
处理流程:
- 1.前端进行请求时,首先是我们的UsernamePasswordAuthenticationFilter进行拦截我们的请求,里面有一个attemptAuthentication方法 封装我们请求数据,一般是是用户名与密码,具体代码如下。
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());
} else {
String username = this.obtainUsername(request);
String password = this.obtainPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
this.setDetails(request, authRequest);
//这里就是我们封装的数据,把用户名、密码存放在一个UsernamePasswordAuthenticationToken对象里面,保存起来,并调用authenticate方法进行验证。
return this.getAuthenticationManager().authenticate(authRequest);
}
}
- 2.点击进入AuthenticationManager接口,你会看到这里有一个authenticate方法,用来验证用户名和密码。
public interface AuthenticationManager {
Authentication authenticate(Authentication var1) throws AuthenticationException;
}
- 3.AuthenticationManager接口实现主要是由ProviderManager类实现的,它重写了里面的authenticate验证方法。
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
Authentication result = null;
Authentication parentResult = null;
boolean debug = logger.isDebugEnabled();
Iterator var7 = this.getProviders().iterator();
while(var7.hasNext()) {
AuthenticationProvider provider = (AuthenticationProvider)var7.next();
//判断是否为我们需要验证的数据类型。就是我们自己封装的
if (provider.supports(toTest)) {
if (debug) {
logger.debug("Authentication attempt using " + provider.getClass().getName());
}
try {
//1.实际上,是AuthenticationProvider的authenticate方法对用户信息进行验证
result = provider.authenticate(authentication);
if (result != null) {
//验证通过,进行数据复制
this.copyDetails(authentication, result);
break;
}
} catch (AccountStatusException var12) {
this.prepareException(var12, authentication);
throw var12;
} catch (InternalAuthenticationServiceException var13) {
this.prepareException(var13, authentication);
throw var13;
} catch (AuthenticationException var14) {
lastException = var14;
}
}
}
if (result == null && this.parent != null) {
try {
result = parentResult = this.parent.authenticate(authentication);
} catch (ProviderNotFoundException var10) {
;
} catch (AuthenticationException var11) {
lastException = var11;
}
}
if (result != null) {
if (this.eraseCredentialsAfterAuthentication && result instanceof CredentialsContainer) {
((CredentialsContainer)result).eraseCredentials();
}
if (parentResult == null) {
this.eventPublisher.publishAuthenticationSuccess(result);
}
return result;
} else {
if (lastException == null) {
lastException = new ProviderNotFoundException(this.messages.getMessage("P**想要知道更多技术,请关注我的紫晶公众号:East_Amethyst Object[]{toTest.getName()}, "No AuthenticationProvider found for {0}"));
}
this.prepareException((AuthenticationException)lastException, authentication);
throw lastException;
}
}
- 4可以看到provider.supports(toTest)是验证我们封装的数据类型是否为authention类型的,通过AuthenticationProvider这个类的authenticate,我们获取用户的验证结果,是通过还是不通过,点进这个方法进行查看,看到这是authenticationProvider接口提供的一个方法,,
public interface AuthenticationProvider {
Authentication authenticate(Authentication var1) throws AuthenticationException;
boolean supports(Class<?> var1);
}
这个接口由AbstractUserDetailsAuthenticationProvider类进行实现,重写了authentication方法,有时候没有进行重写,则是由AbstractUserDetailsAuthenticationProvider这个类的子类DaoAuthenticationProvider进行重写,具体看你使用的jdk版本,我的是tractUserDetailsAuthenticationProvider进行重写的,源码如下:
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, () -> {
return this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported");
});
String username = authentication.getPrincipal() == null ? "NONE_PROVIDED" : authentication.getName();
boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
//从数据库获取数据
user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
} catch (UsernameNotFoundException var6) {
this.logger.debug("User '" + username + "' not found");
if (this.hideUserNotFoundExceptions) {
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
throw var6;
}
Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
}
try {
this.preAuthenticationChecks.check(user);
//验证用户名密码是否正确
this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
} catch (AuthenticationException var7) {
if (!cacheWasUsed) {
throw var7;
}
cacheWasUsed = false;
user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
this.preAuthenticationChecks.check(user);
this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
}
this.postAuthenticationChecks.check(user);
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (this.forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
//把验证的信息保存在封装数据的result.setDetails中
//一般这个封装的类UsernamePasswordAuthenticationToken继承AbstractAuthenticationToken,里面有个Details属性用来保存验证前 后数据。
//实现Authentication
return this.createSuccessAuthentication(principalToReturn, authentication, user);
}
- 5.查看我们的this.retrieveUser方法,我在代码中都进行了说明,你会发现调用了loadUserByUsername方法从数据库获取数据,调用additionalAuthenticationChecks方法对我们的密码进行验证,这个方法我们可以重写。
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
this.prepareTimingAttackProtection();
try {
//调用loadUserByUsername方法
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
} else {
return loadedUser;
}
} catch (UsernameNotFoundException var4) {
this.mitigateAgainstTimingAttack(authentication);
throw var4;
} catch (InternalAuthenticationServiceException var5) {
throw var5;
} catch (Exception var6) {
throw new InternalAuthenticationServiceException(var6.getMessage(), var6);
}
}
- 6.这个loadUserByUsername方法我们可以看到是在UserDatailsService这个里面实现的。
public interface UserDetailsService {
UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}
- 这个类有好几个实现类,当然我们也可以自定义实现类,从我们自己的数据库中回去数据,进行返回。
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
if (authentication.getCredentials() == null) {
this.logger.debug("Authentication failed: no credentials provided");
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
} else {
String presentedPassword = authentication.getCredentials().toString();
if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
this.logger.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
}
}
- additionalAuthenticationChecks方法对密码进行验证,验证我们前端输入的数据是否为我们数据库里,如果是一致,就把我们的验证信息保证在AbstractAuthenticationToken类的Details属性中,验证通过,后台返回数据到前端。
- 当我们把用户信息保存了,在以后要用的时候,有三种方式获取用户验证的信息,实现验证信息不多个请求中共享。
- ==通过SecurityContextHolder.getContext().getAuthentication();==
- ==方法参数(Authentication authtication)==
- ==通过在方法参数里注解获取(@AuthticationPrincipal UserDetails user)==
- 6.这就是SpringSecurity的验证流程源码,如果你想要了解自定义的验证流程,你可以关注我的紫晶公众号:East_Amethyst,或者有时间了再来写在这里,请关注我哦。里面有很多免费的东西,都是写干货。
SpringSecurity应用场景
- 1.我们可以用这个框架来拦截我们前端的请求,封装请求数据,对用户进行认证功能。
- 2.不同用户请求,我们设置不同的权限,当然角色-权限-操作是存于数据里里,不同的权限,我们设置不同的操作,返回不同的数据给前端。
- 3.现在请求访问,允许哪些请求要验证,哪些请求不用验证之类的,相当于拦截器的功能。
为什么要用SpringSecurity
- 使用SpringSecurity安全验证可以自定义我们的认证,验证逻辑,对不同的请求做不同的逻辑处理,保存用户验证信息等,当然了学习难度开始还是比较大的,不容易理解。
分析问题,提问
- 为什么使用这个框架,我们启动系统的时候,会自动跳到我们框架中的登录页面进行登录呢?
- 回答:在East_Amethyst公众号有对这些问题进行解答,,,
如想看更多可关注紫晶猿的紫晶服务公众号:East_Amethyst