SpringSecurity(一)——基本的登录使用
一、简述SpringSecurity
SpringSecurity是一个能够为基于SPring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它充分利用了Spring的IOC、DI和AOP功能,为系统提供了声明式的安全访问控制功能,减少了为企业系统安全控制开发大量的重复代码。
SpringSecurity对Web安全性的支持大量的依赖于Servlet的Filter。这些过滤器拦截进入系统的请求,并且在处理请求钱进行某些安全的验证。SpringSecurity提供了多个过滤器,这些过滤器拦截了Servlet请求,并将这些请求转给认证和访问决策管理器处理,从而增强安全性。这也使得开发人员可以根据自己的需求,适当的使用过滤器来进行自我的保护。
二、开始
1、导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
2、启动项目
默认的用户为:user
3、入门流程图
注释:
- UsernamePasswordAuthenticationFilter: 是用户名和密码的主要处理类,构造出UsernamePasswordAuthenticationToken类,将用户的信息封装到Authentication中;
- Authentication接口:封装了用户的信息;
- AuthenticationManager接口:定义了Authentication认证的方法,认证的出发点,也是认证的核心接口。日常登录的方式存在:用户名和密码、手机号和密码、邮箱和密码等多种方式。
- DaoAuthenticationProvider::用于解析并认证 UsernamePasswordAuthenticationToken 的这样一个认证服务提供者,对应以上的几种登录方式。
- UserDetailsService接口:SpringSecurity会将username传递给后端UserDetailsServiceImpl类的loadbyUsername方法,根据提供的username到数据库中查询出指定的用户信息,并将信息封装到UserDetails对象中返回给SpringSecurity,并且由SpringSecurity完成密码的比对;
- UserDetails接口:用户的核心信息。根据username在UserDetailsService的loadByUsername中查询对应的数据封装到UserDetails对象中,最后封装到Authentication对象中。
7.
UsernamePasswordAuthenticationFilter: 是我们最常用的用户名和密码认证方式的主要处理类,构造了一个UsernamePasswordAuthenticationToken对象实现类,将用请求信息封装为Authentication;
BasicAuthenticationFilter…: 将UsernamePasswordAuthenticationFilter的实现类UsernamePasswordAuthenticationToken封装成的 Authentication进行登录逻辑处理;
ExceptionTranslationFilter: 主要用于处理AuthenticationException(认证)和AccessDeniedException(授权)的异常;
FilterSecurityInterceptor: 获取当前 request 对应的权限配置**,**调用访问控制器进行鉴权操作;
4、创建登录表
CREATE TABLE `sys_user` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
`user_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '用户名',
`password` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '密码',
`email` varchar(64) DEFAULT NULL COMMENT '邮箱',
`phonenumber` varchar(32) DEFAULT NULL COMMENT '手机号',
`sex` char(1) DEFAULT NULL COMMENT '用户性别(0男,1女,2未知)',
`avatar` varchar(128) DEFAULT NULL COMMENT '头像',
PRIMARY KEY (`id`)
) ENGINE=InnoDB COMMENT='用户表';
5、完成基本的dao、Mapper、测试可以正常使用
- 创建User实体类
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("sys_user")
public class User implements Serializable {
private static final long serialVersionUID = -40356785423868312L;
/**
* 主键
*/
@TableId
private Long id;
/**
* 用户名
*/
private String userName;
/**
* 密码
*/
private String password;
/**
* 邮箱
*/
private String email;
/**
* 手机号
*/
private String phonenumber;
/**
* 用户性别(0男,1女,2未知)
*/
private String sex;
/**
* 头像
*/
private String avatar;
}
- 因为SpringSecurity需要返回的为UserDetails
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginUser implements UserDetails {
private User user;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUserName();
}
/**
* 是否未过期
* @return
*/
@Override
public boolean isAccountNonExpired() {
return true;
}
/**
* 是否未锁定
* @return
*/
@Override
public boolean isAccountNonLocked() {
return true;
}
/**
* 凭证是否未过期
* @return
*/
@Override
public boolean isCredentialsNonExpired() {
return true;
}
/**
* 是否可用
* @return
*/
@Override
public boolean isEnabled() {
return true;
}
}
- 实现UserDetailService接口并且重写loadUserByUsername方法
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
/**
* 通过重写loadByUsername方法来自定义登录的逻辑
* @param username
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 到数据库中根据用户名查询用户信息
User userInfo = userMapper.selectOne(new QueryWrapper<User>().eq("user_name", username));
//如果没有查询到用户
if (Objects.isNull(userInfo)){
throw new RuntimeException("用户名或者密码错误");
}
// 将用户信息封装到UserDetails中 返回给SpringSecurity
return new LoginUser(userInfo);
}
}
注: 到此基本搭建完成,但是SpringSecurity的密码是存储的加密的,若使用明文密码需要在密码的前面添加前缀“{noop}”。
6、针对密码的处理
SpringSecurity提供了加密的方式,一般采用的是BCryptPasswordEncoder,将BCryptPasswordEncoder注入到Spring中,SpringSecurity就会使用PasswordEcoder进行密码的校验。
可以通过定义一个SpringSecurity的配置类,SpringSecurity要求这个配置类要继承WebSecurityConfigurerAdapter。
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
7、准备登录接口
Controller层
@PostMapping("/login")
public ResponseResult login(@RequestBody User user) {
ResponseResult login = loginService.login(user);
return login;
}
流程:
1、SecurityConfig 继承 WebSecurityConfigurerAdapter 并重写configure()方法,对login接口进行放行,因为用户认证需要使用AuthenticationManager的authenticate方法,因此需要在SecurityConfig配置中将AuthenticationManager注入到容器。
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
//关闭csrf
.csrf().disable()
//不通过Session获取SecurityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 对于登录接口 允许匿名访问
.antMatchers("/login").anonymous()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated();
// 把token校验过滤器添加到过滤器链中
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
2、认证过滤器:
需要自定义一个过滤器,这个过滤器会去获取请求头中的token,对token进行解析取出其中的userid。使用userid去redis中获取对应的LoginUser对象。然后封装Authentication对象存入SecurityContextHolder
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private RedisCache redisCache;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 获取token
String token = request.getHeader("token");
// 判断token是否为空
if (StringUtils.hasText(token)) {
//放行
//解析token
String userid;
try {
Claims claims = JwtUtil.parseJWT(token);
userid = claims.getSubject();
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("token非法");
}
//从redis中获取用户信息
String redisKey = "login:" + userid;
LoginUser loginUser = redisCache.getCacheObject(redisKey);
if(Objects.isNull(loginUser)){
throw new RuntimeException("用户未登录");
}
//封装Authentication对象存入SecurityContextHolder
//TODO 获取权限信息封装到Authentication中
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginUser,null,null);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
//放行
filterChain.doFilter(request, response);
}
}
SecurityConfig中configure中添加过滤
// 把token校验过滤器添加到过滤器链中
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
3、Service层
/**
* 登录 如果认证通过,使用user生成jwt jwt存入ResponseResult 返回
* @param user
* @return
*/
@Override
public ResponseResult login(User user) {
// 通过UsernamePasswordAuthenticationToken获取用户名和密码
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword());
//AuthenticationManager委托机制对authenticationToken 进行用户认证
Authentication authentication = authenticationManager.authenticate(usernamePasswordAuthenticationToken);
//如果认证没有通过,给出对应的提示
if (Objects.isNull(authentication)){
throw new RuntimeException("登录失败");
}
//如果认证通过,拿到这个当前登录用户信息
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
//获取当前用户的userid
String userid = loginUser.getUser().getId().toString();
String jwt = JwtConfig.createJWT(userid);
Map<String, String> map = new HashMap<>();
map.put("token",jwt);
//把完整的用户信息存入redis userid为key 用户信息为value
redisCache.setCacheObject("login:"+userid,loginUser);
return new ResponseResult(200,"登录成功",map);
}
8、退出操作
ServiceImpl:
/**
* 退出
* @return
*/
@Override
public ResponseResult logout() {
//从SecurityContextHolder中的userid
UsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken)SecurityContextHolder.getContext().getAuthentication();
//
LoginUser loginUser = (LoginUser)authentication.getPrincipal();
Long userId = loginUser.getUser().getId();
redisCache.deleteObject("login:" + userId);
return new ResponseResult(200, "注销成功");
}