Spring Security
0.简介
spring security是spring家族的一个安全管理框架。
一般web应用需要进行认证和授权,而认证和授权也是spring security作为安全框架的核心功能。
认证:验证当前用户是否是本系统的用户,并要确定具体是哪个用户
授权:经过认证后判断当前用户是否有权限进行某个操作
1.入门
在spring boot项目中引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
再次启动时,就会跳转到login页面

默认用户名:user
默认密码:控制台输出
2.认证
2.1原理初探

UsernamePasswordAuthenticationFilter:判断用户名密码是否正确
ExceptionTranslationFilter:处理认证过程中出现的异常
FilterSecurityInterceptor:校验用户的权限,授权
以上只是核心过滤器,可以通过debug看到实际上有16个过滤器

2.2入门案例的认证流程

- 在默认的流程中,提交的用户名密码封装成
Authentication,通过多层传递,最后通过UserDetailsService的实现类InMemoryUserDetailsManager根据用户名在内存中查找对应的用户信息以及用户权限信息。把用户信息和用户对应的权限信息封装成UserDetails对象。 - 返回
UserDetails对象,比较UserDetails中的密码与提交的Authentication中的密码是否一致,如果一致,就把UserDetails中的权限信息设置到Authentication对象中。 - 返回
Authentication对象,把该对象存储在上下文中。
2.3我们的需求
- 前端传递的用户名密码要通过数据库查询
- 认证通过后,返回一个token给前端
2.3.1需求的实现思路:
通过自定义实现`UserDetailsService`接口实现
返回的`Authentication`对象在`UsernamePasswordAuthenticationFilter`没有实现生成token,传递给前端的相关方法。所以我们需要自己定义一个Filter,接收`Authentication`对象,并实现生成token等。

2.3.2实现时考虑权限校验

用户登录后进行操作:
用户携带token发送请求,后端解析token,获取userId后,如何获得用户的权限信息?
1. 可以通过userId去数据库查询用户的权限,但这样每次操作都要查询数据库,对数据库造成很大压力 //×
2. 从redis中获取用户的权限数据 //√
所以需要在用户认证时,把用户的相关信息存入redis中

2.3.3最终思路
登录:
1.自定义登录接口
通过ProviderManager的方法进行认证,如果认证通过,生成JWT
把用户的信息存入redis
2.自定义UserDetailsService
自定义这个接口的实现类,实现去数据库中查询用户
校验:
1.定义JWT认证过滤器
获取token,解析token获取userId
从redis中获取用户权限等信息
把用户权限等信息存入SecurityContextHolder (因为后面的过滤器需要从context中获取用户信息)
2.3.4具体实现
自定义UserDetailsService:
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private SysUserService userService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//1.根据用户名查询数据库用户
SysUser user = userService.getUserByUserName(username);
if(null == user) {
throw new UsernameNotFoundException("用户名不存在!");
}
//TODO 2.根据用户id查询用户操作权限数据
//把用户和用户的权限封装成UserDetails类型返回
return new CustomUser(user);
}
}
由于返回时需要的是UserDetails类型,需要把查询到的用户封装到UserDetails中
定义一个类,实现UserDetails接口
或者定义一个类,继承User类(因为User内部实现了UserDetails接口)
public class CustomUser2 implements UserDetails {
private SysUser user;
public SysUser getUser() {
return user;
}
public void setUser(SysUser user) {
this.user = user;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
.......
public class CustomUser extends User {
/**
* 我们自己的用户实体对象,要调取用户信息时直接获取这个实体对象。
*/
private SysUser sysUser;
public CustomUser(SysUser sysUser, Collection<? extends GrantedAuthority> authorities) {
super(sysUser.getUsername(), sysUser.getPassword(), authorities);
this.sysUser = sysUser;
}
public SysUser getSysUser() {
return sysUser;
}
public void setSysUser(SysUser sysUser) {
this.sysUser = sysUser;
}
}
以上就实现了自定义UserDetailsService,在测试时,要把数据库中的密码前面加上{noop}
这样自定义的UserDetailsService会把原来默认的UserDetailsService替换掉,因为SpringBoot会按条件自动装配,如果存在自定义的UserDetailsService,就不会再注册自带的。
默认实现类有@ConditionalOnMissingBean,自定义的加上@Service注解,spring会自动注册这个类
密码加密存储
上述在测试时,需要把数据库中的密码前面加上{noop}
因为默认使用的PasswordEncoder要求数据库中的密码格式为:{id}password。它会根据id判断密码的加密方式。但是一般我们不使用这种方式,所以需要替换掉PasswordEncoder。
我们一般使用spring-security提供的BCryptPasswordEncoder,只需把BCryptPasswordEncoder对象注入Spring容器,就会使用该PasswordEncoder进行密码校验。
定义一个SpringSecurity配置类,继承WebSecurityConfigurerAdapter,把BCryptPasswordEncoder注入
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
自定义登录接口
- 登录时,需要spring-security对登录接口放行。登录页面不需要认证。
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) throws Exception {
http
//关闭csrf
.csrf().disable()
//不通过Session获取SecurityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 对于登录接口 允许匿名访问
.antMatchers("/login").anonymous()//允许匿名用户访问,不允许已登入用户访问
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated();
}
}
- 自定义登录接口,其实就是自定义controller,controller调用自定义service,service实现AuthenticationManager的authenticate方法来进行用户认证。
所以需要在SpringSecurity中配置AuthenticationManager容器
在自定义的接口中通过AuthenticationManager的authenticate方法进行认证
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
....
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
@Resource
private AuthenticationManager authenticationManager;
@Override
public ResponseResult login(User user) {
/** 1.AuthenticationManager的authenticate进行用户认证
authenticate需要传入authenticate类型参数。authenticate是接口,需要它的实现类UsernamePasswordAuthenticationToken
传入UsernamePasswordAuthenticationToken的参数只有用户名和密码
2.认证通过,使用userId生成JWT
3.用户信息存入redis
**/
//1
Authentication authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword());
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
//2
if (null == authenticate){
throw new RuntimeException("认证失败");
}
MyUserDetails myUserDetails = (MyUserDetails) authenticate.getPrincipal();
String id = myUserDetails.getUser().getId().toString();
String token = new JWTUtil().createToken(id);
//3
HashMap<String, Object> map = new HashMap<>();
map.put("token",token);
return new ResponseResult(200,"认证成功",map);
}
}
测试

浙公网安备 33010602011771号