springsecurity简介
springsecurity是一个安全框架,解决系统安全问题的框架。
springsecurity是spring家族的一员,是一个能够为基于spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在spring应用上下文中配置的Bean,充分利用了Spring IOC、DI(控制反转,依赖注入)和AOP功能。SpringSecurity两大核心功能是“认证、授权”
SpringSecurity入门
引入springsecurity依赖即可,我们访问接口的时候就会自动跳转到springsecurity自带的登录界面,用户名为user,密码为控制台打印的,每次启动项目打印的密码不一样。
查看springsecurity源码我们可以发现之所以会跳转到SpringSecurity自带的登录界面是因为执行了UserDetailService,UserDetailService里面有一个loadByUsername方法,返回的是一个UserDetails对象(其实是一个接口),里面首先校验用户名,用户名错误直接抛出异常,然后校验密码。校验密码是利用PasswordEncoder接口,里面有encode方法,该方法作用是去加密客户端明文密码,然后里面还有一个matches方法,传的参数就是客户端的明文密码和加密后的密码去匹配,里面还有个upgradeEncoding方法,该方法是二次加密,默认返回false。PasswordEncoder的实现类有很多,官方推荐的是BCryptPasswordEncoder密码加密器实现类,该加密器默认的密码长度是10,使用该加密器加密出来的密码自带随机盐(密码长度,版本号,随机字符串),每次加密出来的密码不一样。
SpringSecurity自定义登录逻辑
首先我们创建一个Security配置类,声明一个bean交给spring容器去管理,里面就是返回一个密码加密器对象,然后我们写一个Service去实现UserDetailService接口,重写loadByUsername方法,根据用户名去数据库查询,如果查询不存在就抛出UsernameNotFoundExection异常,然后使用密码加密器去比较密码,匹配成功返回UserDetails。
SpringSecurity自定义登录页面
首先我们在Security配置类中去继承WebSecurityConfigurerAdapter然后重写configure方法,该方法里面我们设置http表单提交自定义登录页面即可。然后我们会发现设置了自定义登录页面以后所有请求没有授权都可以访问,原因是我们重写configure方法里面没有设置授权,设置除了登录请求不需要授权以外,其他请求都要认证才能访问(一般使用ant风格写法,?代表匹配单个字符,*代表匹配0个或多个字符,**代表匹配0个或多个目录),还需要设置关闭csrf跨站请求伪造。
SpringSecurity自定义登录成功跳转
配置自定义成功跳转我们只需要写一个认证成功处理器去实现AuthenticationSuccessHandler然后重写onAuthenticationSuccess方法,让response请求去重定向需要跳转的页面,然后在我们的配置类中去定义认证成功以后认证成功处理器使用我们自己定义的即可。
SpringSecurity权限控制
在security配置类中,使用antMatchers指定具体页面然后设置权限,该权限设置严格区分大小写,它会去匹配我们登录逻辑里面返回的UserDetails里面设置的权限去匹配。
我们也可以设置基于角色去控制访问,我们在自定义登录逻辑里面匹配成功则设置一个角色,设置角色必须以ROLE_开头,并且严格区分大小写。当然SpringSecurity也可以使用注解的方式进行权限控制,我们需要在启动类上加上@EnableGlobalMethodSecurity(securedEnable=true)注解开启,然后再到需要进行权限控制的方法或者Controller上加上@Secured(“ROLE_具体角色”)注解即可,该注解相当于配置类中的hasRole。
Oauth2简介
第三方认证技术方案最主要是解决认证协议的通用标准问题,因为要实现跨系统认证,各系统之间要遵循一定的接口协议。Oauth协议为用户资源的授权提供了一个安全的、开放而又简易的标准。同时,任何第三方都可以使用Oauth认证服务,任何服务提供商都可以实现自身的Oauth认证服务,因而Oauth是开放的。
Oauth2认证流程
客户端相当于网页或者手机端。资源拥有者相当于用户,也可以是应用程序,即该资源的拥有者。授权服务器用来对资源拥有的身份进行认证、对访问资源进行授权,客户端想要访问资源需要通过授权服务器由资源拥有者授权后可访问。资源服务器就是存储资源的,比如存储用户的信息,客户端最终访问资源服务器获取信息。
通俗点讲:比如一个客户端想要获取到某个微信用户的基础信息,用户允许网页授权,然后拿到授权码去微信认证服务器认证,认证成功获得访问令牌,然后拿着令牌去访问资源服务器,资源服务器校验令牌合法会返回给我们相应的信息。
Oauth2授权模式
1.授权码模式 Authorization Code(该模式最复杂也是最安全的)
授权码模式首先客户端通过浏览器带上客户端身份以及重定向URI去认证服务器,认证服务器会让资源拥有者(用户)去认证,用户认证完会返回一个授权码,客户端拿到授权码以后根据重定向的URI和授权码去授权服务器根据之前颁发的授权码进行校验通过返回一个访问令牌或者刷新令牌。
2.密码模式 Resource Owner PasswordCredentials(该模式适用于同一项目不同端)
密码模式用户在客户端输入密码,客户端根据用户输入的密码直接到授权服务器,授权服务器返回访问令牌或者刷新令牌。
3.简化模式
4.客户端模式
SpringSecurityOauth2整合
首先需要导入maven依赖如下
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
然后我们需要编写一个service去实现UserDetailsService,目的是为了自定义登录逻辑,不使用Security默认的登录逻辑,代码如下:
/**
* @Author Fausto
* @Date 2022/4/9 18:34
* @Comment 自定义登录Service逻辑
* @Version 1.0
*/
@Service
public class UserService implements UserDetailsService {
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//由于没有连接数据库,所以直接自定义一个密码
String password = passwordEncoder.encode("123456");
return new User(username,password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
}
}
由于自定义登录逻辑里面我们需要返回一个User对象,我们可以自定义一个User对象去实现UserDetails接口
/**
* @Author Fausto
* @Date 2022/4/9 18:42
* @Comment
* @Version 1.0
*/
public class User implements UserDetails {
private String username;
private String password;
private List<GrantedAuthority> authority;
public User(String username, String password, List<GrantedAuthority> authority) {
this.username = username;
this.password = password;
this.authority = authority;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authority;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
然后我们需要写一个SecurityConfig的配置类去继承WebSecurityConfigurerAdapter,里面需要把加密器配置成bean交给spring容器管理,然后重写configure方法,里面使用ant表达式去放行一些必要资源
/**
* @Author Fausto
* @Date 2022/4/9 18:37
* @Comment
* @Version 1.0
*/
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//放行"/oauth/**","/login/**","/logout/**"路径
http.authorizeRequests().antMatchers("/oauth/**","/login/**","/logout/**")
//其余请求都需要认证
.permitAll().anyRequest().authenticated()
//所有表单请求放行
.and().formLogin().permitAll()
//csrf关闭
.and().csrf().disable();
}
}
然后我们需要配置一个认证服务器,类上需要加上@EnableAuthorizationServer注解
/**
* @Author Fausto
* @Date 2022/4/9 18:52
* @Comment 授权服务器
* @Version 1.0
*/
@Configuration
@EnableAuthorizationServer //开启授权服务器
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserService userService;
@Autowired
@Qualifier("jwtTokenStore")
private TokenStore tokenStore;
@Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
//放内存中
clients.inMemory()
//客户端ID
.withClient("client")
//密钥123
.secret(passwordEncoder.encode("123"))
//重定向地址
.redirectUris("http://www.baidu.com")
//范围
.scopes("all")
//令牌失效时间
.accessTokenValiditySeconds(60)
//刷新令牌失效时间
.refreshTokenValiditySeconds(86400)
/**
* 授权类型,可以同时支持多种模式
* authorization_code:授权码模式
* password:密码模式
*refresh_token:刷新令牌
*/
.authorizedGrantTypes("authorization_code","password","refresh_token");
}
/**
* 密码模式
* @param endpoints
* @throws Exception
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager)
.userDetailsService(userService)
//把token转成jwttoken
.tokenStore(tokenStore)
.accessTokenConverter(jwtAccessTokenConverter);
}
}
由于我们是把授权服务器生成的token转成Jwt的所以我们再配置一个Jwt配置类
/**
* @Author Fausto
* @Date 2022/4/10 11:50
* @Comment jwt配置类,把oauth2生成的token转成jwt令牌
* @Version 1.0
*/
@Configuration
public class JwtTokenStoreConfig {
@Bean
public TokenStore jwtTokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
//设置jwt密钥
jwtAccessTokenConverter.setSigningKey("test_key");
return jwtAccessTokenConverter;
}
}
然后我们再配置一个资源服务器,资源服务器里面拦截了所有请求需要认证,然后就放行了user接口
/**
* @Author Fausto
* @Date 2022/4/9 18:58
* @Comment 资源服务器配置
* @Version 1.0
*/
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and().requestMatchers().antMatchers("/user/**");
}
}
下面是UserController
/**
* @Author Fausto
* @Date 2022/4/9 19:02
* @Comment
* @Version 1.0
*/
@RestController
@RequestMapping("/user")
public class UserController {
@RequestMapping("/getCurrentUser")
public Object getCurrentUser(Authentication authentication){
return authentication.getPrincipal();
}
}
好了接下来我们使用密码模式用PostMan去测试
Authorization这里需要填写我们授权服务器里面设置的账号密码
需要填写一些必要的参数,认证模式我们写密码模式
获取了jwt生成的token和刷新令牌
把token输入进去,获取到了资源。