springboot 集成Security 框架
之前的项目都是用的Security 作为鉴权框架,项目中实际也用了四五个了,可是发现每次用的时候都记不住了,所以还是记下来吧,作为自己的笔记吧
springboot 先引入Security依赖,Security 已经有了boot 的starter 包直接可以引入
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- security 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
什么都不用配置,启动项目,这个时候security已经起作用了。随便访问项目的一个地址,可以看到都被定向到了登陆页面。
账户user ,密码是控制打印的,确保日志打印级别为info就能看到。
现在只有一个固定的user用户,密码每次都是启动时随即生成的,真正的项目肯定无法使用的,现在来对security 进行配置。
新建一个配置类SecurityConfig 继承于 WebSecurityConfigurerAdapter
package z.c.security.config;
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;
/**
* anyRequest | 匹配所有请求路径
* access | SpringEl表达式结果为true时可以访问
* anonymous | 匿名可以访问
* denyAll | 用户不能访问
* fullyAuthenticated | 用户完全认证可以访问(非remember-me下自动登录)
* hasAnyAuthority | 如果有参数,参数表示权限,则其中任何一个权限可以访问
* hasAnyRole | 如果有参数,参数表示角色,则其中任何一个角色可以访问
* hasAuthority | 如果有参数,参数表示权限,则其权限可以访问
* hasIpAddress | 如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
* hasRole | 如果有参数,参数表示角色,则其角色可以访问
* permitAll | 用户可以任意访问
* rememberMe | 允许通过remember-me登录的用户访问
* authenticated | 用户登录后可访问
*/
/**
* Security 配置
* @author zhoupan
* @data 2021/8/11 16:19
* @description
*/
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter
{
@Override
protected void configure(HttpSecurity http) throws Exception
{
// String redirectUrl = securityProperties.getBrowser().getLoginPage();
//basic 登录方式
// http.httpBasic()
http
//开启表单登录方式
.formLogin()
//自定义的登陆页跳转地址,不设置默认为/login,设置后需要将设置地址设置为任何人可以访问,不然会重定向循环
.loginPage("/login")
//登录提交的请求,不设置时和loginPage设置的地址一样,单独设置没有意义,还发现一个挺奇怪的点,当配置了successHandler时,如果没有在successHandler处理器中跳转其他地址,那么浏览器就会跳转到loginProcessingUrl配置的地址,这是个虚地址, 你可以没有该地址
.loginProcessingUrl("/login/form")
// 认证成功的转发地址 ,如果没有设置则按照跳转到登陆前的地址。即会记忆地址,登陆后又到原地址。这里有
//个坑如果这个地址最终返回的是个视图则会报错 ,因为登陆是post请求,而mvc不允许post请求返回视图 ,只要要改为转发就行 。
.successForwardUrl("/test/a")
//默认的认证成功跳转路径,这里和successForwardUrl的区别是,如果加上参数true效果是一样的,如果不加,则没有记忆地址时会跳到该地址。
//successForwardUrl和defaultSuccessUrl两个都设置则以defaultSuccessUrl为准
.defaultSuccessUrl("/test/b",true)
//登录成功处理器,设置了登录认证成功就会执行,在这里可以直接返回,配置了这个处理器后,
//loginProcessingUrl和defaultSuccessUrl就不起作用了
// .successHandler(mySuccessHandler)
//登录失败处理器
// .failureHandler(myFailHandler)
.and()
//请求授权
.authorizeRequests()
//不需要权限认证的url
.antMatchers("/login","/login/form").permitAll()
//其他的任何请求 需要身份认证
.anyRequest().authenticated()
.and()
//关闭跨站请求防护
.csrf().disable();
}
}
代码中有详细的注释,可以看到开启了表单登录后,可以对登录地址,登录请求地址,登录成功跳转地址以及失败成功处理器的设置(当然还有很多其他的配置这里没有写)
基于这个配置先实现自定义用户登录验证的功能,只需要实现UserDetailsService类,来代替默认的用户信息类来获取用户
@Component
public class MyUserDetailsService implements UserDetailsService
{
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException
{
//这里模拟从数据库跟据名字查用户,我把密码给成了一样,权限给空
UserDetails userDetails = new User(s,s, Collections.emptyList());
return userDetails;
}
}
这个类有个loadUserByUsername方法,就是通过用户账户获取用户,即通过前端提交的用户账户。找到对应的用户,返回一个UserDetails接口对象,注入了MyUserDetailsService后Security 会使用注入的这个类获取登陆用户的UserDetails,如果获取到了,就会用这个UserDetails对象和我们前台提交的用户密码进行对比,判断用户密码信息是否正确。也可以手动配置 比如这样:注释掉@Component 自动注入 增加 .userDetailsService(myUserDetailsService())
/**
*注入自定义的用户信息获取器
*/
@Bean
public MyUserDetailsService myUserDetailsService(){
return new MyUserDetailsService();
}
@Override
protected void configure(HttpSecurity http) throws Exception
{
// String redirectUrl = securityProperties.getBrowser().getLoginPage();
//basic 登录方式
// http.httpBasic()
//表单登录 方式
http.
formLogin()
// .loginPage("/login")
//登录需要经过的url请求
.loginProcessingUrl("/login/form")
.successForwardUrl("/test/a")
.defaultSuccessUrl("/test/b",true)
//登录成功处理器
.successHandler(successHandler)
.failureHandler(failHandler)
.and()
//请求授权
.authorizeRequests()
//不需要权限认证的url
.antMatchers("/login","/login/form").permitAll()
//任何请求
.anyRequest()
//需要身份认证
.authenticated()
.and()
==================这里=====================
//在这里增加自定义用户过滤器的配置
.userDetailsService(myUserDetailsService())
==================这里=====================
//关闭跨站请求防护
.csrf().disable();
}
或者是重载另外一个configure方法设置
设置用户服务类和密码编码器
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//设置用户信息服务类
auth.userDetailsService(userService)
//设置密码编码器
.passwordEncoder(myPasswordEncode());
}
这样也可以,启动项目后发现登录是会报错,因为security 的密码默认是BCrypt加密的,自然不会验证通过,并且会提示密码不像个BCrypt加密字符串。有两个办法,一个当然是遵从规则,我们给UserDetails存入BCrypt加密后的密文,这样security拿到前端的密码后进行加密,用加密后的密码和UserDetails中的加密密码进行验证。
修改刚才的获取用户信息代码,这里把账户给加密作为密码,当然真实情况应该是注册用户时把密文存入数据库,BCryptPasswordEncoder这个类就是Security 默认的加密处理类,encode方法就是加密方法
//new BCryptPasswordEncoder().encode(s) 对变量s进行加密,这样就得到了一个加密密码的用户
UserDetails userDetails = new User(s,new BCryptPasswordEncoder().encode(s), Collections.emptyList());
另一个办法就是重新设置密码的验证规则,这里我提供两个途径去实现
- 实现自定义的PasswordEncoder(密码编码器) 替换调Security的默认密码编码器
- 实现AuthenticationProvider(身份认证)接口
先说第一个,只需要实现PasswordEncoder接口,并将其配置到Security中, :配置方法见上面的设置用户信息配置链接.
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
/**
* 自定义的编码器, 模拟明文的验证方式
* @Component 注入就会生效
* @author zhoupan
* @description
* @date 2021/8/12 下午10:16
*/
@Component
public class MyPasswordEncoder implements PasswordEncoder {
/**
* 加密方法,这个方法是用来对前端密码进行加密的
* @param charSequence
* @return
*/
@Override
public String encode(CharSequence charSequence)
{
//我这里不加密 直接返回原密码
return charSequence.toString();
}
/**
* 验证方法
* @param charSequence
* @param s
* @return
*/
@Override
public boolean matches(CharSequence charSequence, String s)
{
//验证密码方式 一样则验证通过
if(s.equals(charSequence.toString()))
{
return true;
}
return false;
}
}
第二个方法,实现AuthenticationProvider后,这时已经不需要配置UserDetailsService的实例了,因为身份认证器已经包括了用户信息和密码认证,都需要在认证方法里去实现,配置了也没有影响,虽然仍然会走默认的密码验证,但是因为我们额外置的AuthenticationProvider,当默认密码验证失败后(这个认证器 DaoAuthenticationProvider),会走我们自己新配的这个身份认证类。当然注释掉是最好的。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
/**
* 自定义密码验证 支持多个身份验证器
* @author zhoupan
* @data 2021/8/12 17:09
* @description
*/
@Component
public class MyAuthenticationProvider implements AuthenticationProvider
{
@Autowired
private MyUserDetailsService myUserDetailsService;
/**
* 认证处理,返回一个Authentication的实现类则代表认证成功,返回null则代表认证失败
* @param authentication
* @return
* @throws AuthenticationException
*/
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException
{
//前端传递的账户和密码
String username = authentication.getName();
String presentedPassword = (String)authentication.getCredentials();
//获取用户信息,这里要换成实际我们的用户信息获取方法
UserDetails user = myUserDetailsService.loadUserByUsername(username);
if(user==null)
{
throw new BadCredentialsException("账户或密码错误");
}
//写自己系统的认证加密等业务逻辑,这里直接判断相等
if(!presentedPassword.equals(user.getPassword()))
{
throw new BadCredentialsException("账户或密码错误");
}
//返回一个经过验证的身份对象 ,这里有重载方法 可以将用户的权限信息设置到这个已经经过验证的权限令牌里
return new UsernamePasswordAuthenticationToken(user, presentedPassword);
}
/**
*
* 如果这个AuthenticationProvider支持指定的Authentication则返回true 支持进一步认证
* 这个注释是源码里翻译的我理解半天也没看懂,后来跟了下源码,ProviderManager的 authenticate方法,是认证方法,登陆时
*UsernamePasswordAuthenticationFilter这个过滤器用账户和密码去构建UsernamePasswordAuthenticationToken对象,注册到ProviderManager中,ProviderManager调用authenticate方法并
*传入UsernamePasswordAuthenticationToken,方法内会先拿到系统内的所有身份验证器,执行AuthenticationProvider的supports方
*法判断是否可以处理,如果不能就接着循环,直到找到为true的,调用其authenticate方法,如果得到的身份令牌不为空,则跳出循环,不再进行其他相同的身份认证器认证了(指
*UsernamePasswordAuthenticationToken身份只要有一个认证成功就成功了) 进行了一个测试看下是否是这样,打开我们刚才的 .
*userDetailsService(myUserDetailsService()) ,系统会先走这个,刚才已经说过了,失败时会接着走我们自己的验证器,现在让系统的登录成功,测试结果没有再进入我们自定义的了。
* @param aClass
* @return
*/
@Override
public boolean supports(Class<?> aClass)
{
return aClass.equals(UsernamePasswordAuthenticationToken.class);
}
}
@Component 注入这个类就会生效了,
现在已经实现了自定义登陆了,可以使用Security 默认的加密,和自定义密码验证,这种是利用表单登陆验证,而现在大多都是前后端分离的,前端项目是个独立的工程,如果用户没有登陆,也不会由后端跳转到登陆页,因为后端项目就没有前端页面,而是直接报401,前端项目收到401后自己路由到登陆页。
这种情况,就不需要开启表单登陆,只需要把登陆接口配置成不拦截,在的登陆方法里利用前面提到的ProviderManager 这个类,手动去调用authenticate方法去认证,怎么拿到ProviderManager的实例,刚好我们继承的WebSecurityConfigurerAdapter这个类就有实例化的方法,
/**
*通过重写authenticationManagerBean方法,利用@Bean将AuthenticationManager交给spring IOC 容器
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception
{
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception
{
// String redirectUrl = securityProperties.getBrowser().getLoginPage();
//basic 登录方式
// http.httpBasic()
//表单登录 方式
http
// formLogin()
// .loginPage("/login")
//登录需要经过的url请求
// .loginProcessingUrl("/login/form")
// .successForwardUrl("/test/a")
// .defaultSuccessUrl("/test/b",true)
//登录成功处理器
// .successHandler(successHandler)
// .failureHandler(failHandler)
// .and()
//请求授权
.authorizeRequests()
//不需要权限认证的url ,放开登陆
.antMatchers("/login").permitAll()
//任何请求
.anyRequest()
//需要身份认证
.authenticated()
.and()
//关闭跨站请求防护
.csrf().disable();
}
通过@Bean 我们注入了AuthenticationManager对象,并且注释掉了表单登陆,然后实现一个登陆接口,
@Autowired
private AuthenticationManager authenticationManager;
@RequestMapping("/login")
private String login(@RequestParam("userName") String userName,@RequestParam("password") String password)
{
System.out.println("账户:"+userName+",密码:"+password);
Authentication authentication = null;
try
{
// 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
authentication = authenticationManager
.authenticate(new UsernamePasswordAuthenticationToken(userName, password));
}
catch (Exception e)
{
if (e instanceof BadCredentialsException)
{
return "账户或密码错误";
}
else
{
return "登陆失败";
}
}
return "登陆成功";
}
这样就实现了自定义登陆接口,通过postman 直接调用接口就可以了
http://localhost:8080/login?userName=aaa&password=aaa
只不过手动调用AuthenticationManager的验证方法后,session 就失效了,下一篇就开始记录下使用token进行身份认证。