赞
踩
Spring Security是一个强大且灵活的身份验证和访问控制框架,用于Java应用程序。它是基于Spring框架的一个子项目,旨在为应用程序提供安全性。
Spring Security致力于为Java应用程序提供认证和授权功能。开发者可以轻松地为应用程序添加强大的安全性,以满足各种复杂的安全需求。
JwtAuthenticationTokenFilter: 这里是我们自己定义的过滤器,主要负责放行不携带token的请求(如注册或登录请求),并对携带token的请求设置授权信息
UsernamePasswordAuthenticationFilter: 负责处理我们在登陆页面填写了用户名密码后的登陆请求。入门案例的认证工作主要由它负责
ExceptionTranslationFilter: 处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException
FilterSecurityInterceptor: 负责权限校验的过滤器。
Authentication接口: 它的实现类表示当前访问系统的用户,封装了用户相关信息。
AuthenticationManager接口: 定义了认证Authentication的方法
UserDetailsService接口: 加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的方法。
UserDetails接口: 提供核心用户信息。通过UserDetailsService根据用户名获取处理的用户信息要封装成UserDetails对象返回。然后将这些信息封装到Authentication对象中。
数据库的采用**RBAC权限模型(基于角色的权限控制)**进行设计。
RBAC至少需要三张表:用户表–角色表–权限表(多对多的关系比较合理)
在配置类中允许注册请求可以匿名访问
registerDTO中存在字符串roleId和实体类user,先取出user判断是否存在相同手机号。若该手机号没有注册过用户,对密码进行加密后即可将用户存入数据库。
创建register方法映射,保存用户的同时也要将roleId一并存入关系表中,使用户获得对应角色。如下图。
@Override public Result register(RegisterDTO registerDTO) { // 获取Map中的数据 User user = registerDTO.getUser(); String roleId = registerDTO.getRoleId(); // 判断是否存在相同手机号 User dataUser = lambdaQuery() .eq(User::getUserPhone, user.getUserPhone()).one(); if (!Objects.isNull(dataUser)) { return Result.fail("该手机号已注册过用户,请勿重复注册"); } // 密码加密 user.setUserPassword(passwordEncoder .encode(user.getUserPassword())); // 将用户及对应角色存入数据库 save(user); userMapper.register(user.getUserPhone(), roleId); return Result.ok("注册成功"); }
在配置类中允许登录请求可以匿名访问
登录流程一般对应认证工作流程
@Resource private AuthenticationManager authenticationManager; @Resource private StringRedisTemplate stringRedisTemplate; @Resource private PasswordEncoder passwordEncoder; @Resource private UserMapper userMapper; @Override public Result login(User user) { //AuthenticationManager 进行用户认证,校验手机号和密码是否正确 UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserPhone(), user.getUserPassword()); Authentication authenticate = authenticationManager.authenticate(authenticationToken); //认证失败给出提示 if (Objects.isNull(authenticate)) { throw new RuntimeException("用户名或密码错误"); } //认证通过,生成jwt并返回 LoginUser loginUser = (LoginUser) authenticate.getPrincipal(); String userId = loginUser.getUser().getUserId(); String jwtToken = JwtUtil.createToken(userId); Map<String, String> map = new HashMap<>(); stringRedisTemplate.opsForValue() .set(LOGIN_CODE_KEY + userId, JSONUtil.toJsonStr(loginUser)); map.put("token", jwtToken); return Result.ok(map); }
先看这段代码: UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserPhone(), user.getUserPassword());
这里先用用户手机号和密码生成UsernamePasswordAuthenticationToken
再看这段代码:Authentication authenticate = authenticationManager.authenticate(authenticationToken);
利用authenticate调用自定义实现类UserDetailsServiceImpl,根据用户名判断用户是否存在(对应认证流程的1、2、3、4)
由于试下的是UserDetailsService接口,所以必须实现其方法loadUserByUsername(根据用户名查询数据库是否存在)这里我传入的是手机号。数据库中若存在用户,则返回UserDetails对象(这里的权限信息暂且不看,对应认证流程的5、5.1、5.2、6)
UserDetails对象返回后,authenticate方法会默认通过PasswordEncoder比对UserDetails与Authentication的密码是否相同。因为UserDetails是通过自定义实现类从数据库中查询出的user对象,而Authentication相当于是用户输入的用户名和密码,也就可以理解为通过前面自定义实现类利用用户名查询到用户后,再看这个用户的密码是否正确。如果用户名或密码不正确,authenticate将会为空,则抛出异常信息。(对应认证流程的7)
由于这里的登录流程不涉及8,9,10,所以不再叙述。
在剩下的代码中我们利用用userId生成了jwt的令牌token,将其存入Redis中并返回token给前端。
除login、register请求外的所有请求都需要携带token才能访问,因此需要设计token拦截器代码,如下。
对于不携带token的请求(如登录/注册)直接放行;对于携带token的请求先判断该用户是否登录,即redis中是否存在相关信息,若存在,将用户授权信息存入SecurityContextHolder,方便用户授权,最后直接放行。
@Component public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { @Resource private StringRedisTemplate stringRedisTemplate; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // 获取token String token = request.getHeader("token"); if (!StringUtils.hasText(token)) { // 没有token,放行 filterChain.doFilter(request, response); return; } // 解析token String userId = null; try { userId = JwtUtil.parseJwt(token); } catch (Exception e) { e.printStackTrace(); System.out.println("token非法:" + e); } // 从redis中获取用户信息 String userJson = stringRedisTemplate .opsForValue().get(LOGIN_CODE_KEY + userId); LoginUser loginUser = JSONUtil.toBean(userJson, LoginUser.class); if (Objects.isNull(loginUser)) { throw new RuntimeException("用户未登录"); } // 存入SecurityContextHolder,设置用户授权信息 UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authenticationToken); // 放行 filterChain.doFilter(request, response); } }
此外,还需将token拦截器设置在过滤器UsernamePasswordAuthenticationFilter的前面。
@Override
public Result logout() {
// 获取SecurityContextHolder中的用户id
UsernamePasswordAuthenticationToken authentication =
(UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
String userId = loginUser.getUser().getUserId();
// 删除redis中的值
stringRedisTemplate
.delete(LOGIN_CODE_KEY + userId);
return Result.ok("注销成功");
}
获取SecurityContextHolder中的用户id后,删除redis中存储的值,即登出成功。
确保实现类正确编写:
@Data @NoArgsConstructor public class LoginUser implements UserDetails { private User user; private List<String> permissions; public LoginUser(User user, List<String> permissions) { this.user = user; this.permissions = permissions; } @JsonIgnore private List<SimpleGrantedAuthority> authorities; @Override public Collection<? extends GrantedAuthority> getAuthorities() { if (authorities != null) { return authorities; } // 把permissions中String类型的权限信息封装成SimpleGrantedAuthority对象 authorities = permissions.stream() .map(SimpleGrantedAuthority::new) .collect(Collectors.toList()); return authorities; } @Override public String getPassword() { return user.getUserPassword(); } @Override public String getUsername() { return user.getUserName(); } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } }
在token拦截器中,我们添加了这段代码。
// 存入SecurityContextHolder,设置用户授权信息
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
这样非登录/注册请求都会被设置授权信息。
为对应接口添加注解@PreAuthorize,就会检验该请求是否存在相关请求。
@Configuration @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig { @Resource private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter; @Resource private AccessDeniedHandlerImpl accessDeniedHandler; @Resource private AuthenticationEntryPointImpl authenticationEntryPoint; @Bean public PasswordEncoder passwordEncoder() { // 实例化PasswordEncoder return new BCryptPasswordEncoder(); } @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http //关闭csrf .csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers("/user/login", "/user/register").anonymous() .anyRequest().authenticated(); // 添加过滤器 http .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); // 配置异常处理器 http .exceptionHandling() .authenticationEntryPoint(authenticationEntryPoint) .accessDeniedHandler(accessDeniedHandler); // 允许跨域 http.cors(); return http.build(); } @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception { // 配置身份验证管理器 return authenticationConfiguration.getAuthenticationManager(); } }
@RestController @RequestMapping("/user") public class UserController { @Resource private IUserService userService; @PostMapping("/login") public Result login(@RequestBody User user) { return userService.login(user); } @GetMapping("/logout") public Result logout() { return userService.logout(); } @PostMapping("/register") public Result register(@RequestBody RegisterDTO registerDTO) { return userService.register(registerDTO); } }
@Data
@NoArgsConstructor
@AllArgsConstructor
public class RegisterDTO {
private User user;
private String roleId;
}
/** * @author modox * @date 2023年6月1日 * @description 封装结果后返回 */ @Data @NoArgsConstructor @AllArgsConstructor public class Result { public static final Integer SUCCESS_CODE = 200; // 访问成功状态码 public static final Integer TOKEN_ERROR = 400; // Token错误状态码 public static final Integer ERROR_CODE = 500; // 访问失败状态码 private Integer status; // 状态码 private String msg; // 提示消息 private Object data = null; public Result(Integer status, String msg) { this.status = status; this.msg = msg; } public static Result ok(Integer status,String msg,Object data){ return new Result(status,msg,data); } public static Result ok(String msg,Object data){ return new Result(SUCCESS_CODE,msg,data); } public static Result ok(Object data){ return new Result(SUCCESS_CODE,"操作成功",data); } public static Result ok(){ return new Result(SUCCESS_CODE,"操作成功",null); } public static Result fail(Integer status,String msg){ return new Result(status,msg); } public static Result fail(String msg){ return new Result(ERROR_CODE,msg); } public static Result fail(){ return new Result(ERROR_CODE,"操作失败"); } public static Map<String,Object> ok(Map<String,Object> map){ map.put("status",SUCCESS_CODE); map.put("msg","查询成功"); return map; } public static Map<String,Object> ok(PageInfo pageInfo){ Map<String,Object> map = new HashMap<>(); map.put("status",SUCCESS_CODE); map.put("msg","查询成功"); map.put("count",pageInfo.getTotal()); map.put("data",pageInfo.getList()); return map; } }
UserDetails的实现类
@Data @NoArgsConstructor public class LoginUser implements UserDetails { private User user; private List<String> permissions; public LoginUser(User user, List<String> permissions) { this.user = user; this.permissions = permissions; } @JsonIgnore private List<SimpleGrantedAuthority> authorities; @Override public Collection<? extends GrantedAuthority> getAuthorities() { if (authorities != null) { return authorities; } // 把permissions中String类型的权限信息封装成SimpleGrantedAuthority对象 authorities = permissions.stream() .map(SimpleGrantedAuthority::new) .collect(Collectors.toList()); return authorities; } @Override public String getPassword() { return user.getUserPassword(); } @Override public String getUsername() { return user.getUserName(); } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } }
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName(value = "grd_menu")
public class Menu {
@TableId
private String menuId;
private String menuName;
private String menuPerms;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName(value = "grd_user")
public class User {
@TableId(type = IdType.ASSIGN_ID)
private String userId;
private String userName;
private Integer userSex;
private String userPhone;
private String userPassword;
private String userSchool;
private Byte[] userImage;
}
@Component public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { @Resource private StringRedisTemplate stringRedisTemplate; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // 获取token String token = request.getHeader("token"); if (!StringUtils.hasText(token)) { // 没有token,放行 filterChain.doFilter(request, response); return; } // 解析token String userId = null; try { userId = JwtUtil.parseJwt(token); } catch (Exception e) { e.printStackTrace(); System.out.println("token非法:" + e); } // 从redis中获取用户信息 String userJson = stringRedisTemplate .opsForValue().get(LOGIN_CODE_KEY + userId); LoginUser loginUser = JSONUtil.toBean(userJson, LoginUser.class); if (Objects.isNull(loginUser)) { throw new RuntimeException("用户未登录"); } // 存入SecurityContextHolder,设置用户授权信息 UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authenticationToken); // 放行 filterChain.doFilter(request, response); } }
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
Result result = new Result(HttpStatus.FORBIDDEN.value(), "您的权限不足");
String json = JSONUtil.toJsonStr(result);
// 处理异常
WebUtils.renderString(response, json);
}
}
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
Result result = new Result(HttpStatus.UNAUTHORIZED.value(), "用户认证失败");
String json = JSONUtil.toJsonStr(result);
// 处理异常
WebUtils.renderString(response, json);
}
}
@Service public class UserDetailsServiceImpl extends ServiceImpl<UserMapper, User> implements UserDetailsService { @Resource private MenuMapper menuMapper; @Override public UserDetails loadUserByUsername(String userPhone) throws UsernameNotFoundException { //根据用户名查询用户信息 QueryWrapper<User> wrapper = new QueryWrapper<>(); wrapper.eq("user_phone", userPhone); User user = getOne(wrapper); //若数据库中不存在用户 if (Objects.isNull(user)) { throw new RuntimeException("该手机号未注册"); } // 根据用户查询权限信息 添加到LoginUser中 List<String> list = menuMapper.selectPermsByUserPhone(user.getUserPhone()); // 封装成UserDetails对象返回 return new LoginUser(user, list); } }
@Service public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService { @Resource private AuthenticationManager authenticationManager; @Resource private StringRedisTemplate stringRedisTemplate; @Resource private PasswordEncoder passwordEncoder; @Resource private UserMapper userMapper; @Override public Result login(User user) { //AuthenticationManager 进行用户认证,校验手机号和密码是否正确 UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserPhone(), user.getUserPassword()); Authentication authenticate = authenticationManager.authenticate(authenticationToken); //认证失败给出提示 if (Objects.isNull(authenticate)) { throw new RuntimeException("用户名或密码错误"); } //认证通过,生成jwt并返回 LoginUser loginUser = (LoginUser) authenticate.getPrincipal(); String userId = loginUser.getUser().getUserId(); String jwtToken = JwtUtil.createToken(userId); Map<String, String> map = new HashMap<>(); stringRedisTemplate.opsForValue() .set(LOGIN_CODE_KEY + userId, JSONUtil.toJsonStr(loginUser)); map.put("token", jwtToken); return Result.ok(map); } @Override public Result logout() { // 获取SecurityContextHolder中的用户id UsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication(); LoginUser loginUser = (LoginUser) authentication.getPrincipal(); String userId = loginUser.getUser().getUserId(); // 删除redis中的值 stringRedisTemplate .delete(LOGIN_CODE_KEY + userId); return Result.ok("注销成功"); } @Override public Result register(RegisterDTO registerDTO) { // 获取Map中的数据 User user = registerDTO.getUser(); String roleId = registerDTO.getRoleId(); // 判断是否存在相同手机号 User dataUser = lambdaQuery() .eq(User::getUserPhone, user.getUserPhone()).one(); if (!Objects.isNull(dataUser)) { return Result.fail("该手机号已注册过用户,请勿重复注册"); } // 密码加密 user.setUserPassword(passwordEncoder .encode(user.getUserPassword())); // 将用户及对应角色存入数据库 save(user); userMapper.register(user.getUserPhone(), roleId); return Result.ok("注册成功"); } }
public class JwtUtil { // token失效:24小时 public static final String token = "token"; public static final long EXPIPE = 1000 * 60 * 60 * 10; public static final String APP_SECRET = "modox@ukc8BDbRigUDaY6pZFfWus2jZWLPHO"; /** * 根据传入的用户Id生成token * @param userId * @return JWT规则生成的token */ public static String createToken(String userId) { String JwtToken = Jwts.builder() .setHeaderParam("typ", "JWT") .setHeaderParam("alg", "HS256") .setSubject("grd_user") .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + EXPIPE)) .claim("userId", userId) .signWith(SignatureAlgorithm.HS256, APP_SECRET) .compact(); return JwtToken; } /** * 验证token是否有效 * @param jwtToken token字符串 * @return 如果token有效返回true,否则false */ public static boolean checkToken(String jwtToken) { try { if (!StringUtils.hasText(jwtToken)) return false; Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken); } catch (Exception e) { e.printStackTrace(); return false; } return true; } /** * 根据token获取User信息 * @param jwtToken token字符串 * @return 解析token获得的user对象 */ public static String parseJwt(String jwtToken) { //验证token if (checkToken(jwtToken)) { Claims claims = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken).getBody(); return claims.get("userId").toString(); }else { throw new RuntimeException("超时或不合法token"); } } }
public class RedisConstants {
public static final String LOGIN_CODE_KEY = "login:code:";
public static final Long LOGIN_CODE_TTL = 2L;
public static final String LOGIN_USER_KEY = "login:token:";
public static final Long LOGIN_USER_TTL = 36000L;
}
public class WebUtils { /** * 将字符串渲染到客户端 * @param response * @param string * @return */ public static String renderString(HttpServletResponse response, String string) { try { response.setStatus(200); response.setContentType("application/json"); response.setCharacterEncoding("utf-8"); response.getWriter().print(string); } catch (IOException e) { e.printStackTrace(); } return null; } }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。