SpringSecurity+Redis+Jwt实现用户认证授权
作者:MoCrane
介绍
Spring Security是一个强大且灵活的身份验证和访问控制框架,用于Java应用程序。它是基于Spring框架的一个子项目,旨在为应用程序提供安全性。
Spring Security致力于为Java应用程序提供认证和授权功能。开发者可以轻松地为应用程序添加强大的安全性,以满足各种复杂的安全需求。
SpringSecurity完整流程
- JwtAuthenticationTokenFilter:这里是我们自己定义的过滤器,主要负责放行不携带token的请求(如注册或登录请求),并对携带token的请求设置授权信息
- UsernamePasswordAuthenticationFilter:负责处理我们在登陆页面填写了用户名密码后的登陆请求。入门案例的认证工作主要由它负责
- ExceptionTranslationFilter:处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException
- FilterSecurityInterceptor:负责权限校验的过滤器。
一般认证工作流程
Authentication接口:它的实现类表示当前访问系统的用户,封装了用户相关信息。
AuthenticationManager接口:定义了认证Authentication的方法
UserDetailsService接口:加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的方法。
UserDetails接口:提供核心用户信息。通过UserDetailsService根据用户名获取处理的用户信息要封装成UserDetails对象返回。然后将这些信息封装到Authentication对象中。
数据库
数据库的采用**RBAC权限模型(基于角色的权限控制)**进行设计。
RBAC至少需要三张表:用户表–角色表–权限表(多对多的关系比较合理)
- 用户表(user):存储用户名、密码等基础信息,进行登录校验
- 角色表(role):对用户的角色进行分配
- 权限表(menu):存储使用不同功能所需的权限
注册流程
配置匿名访问
在配置类中允许注册请求可以匿名访问
编写实现类
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("注册成功"); }
登录流程
配置匿名访问
在配置类中允许登录请求可以匿名访问
调用UserDetailsServiceImpl
登录流程一般对应认证工作流程
@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)
实现UserDetailsServiceImpl
由于试下的是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,就会检验该请求是否存在相关请求。
完整代码
config类
@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(); } }
controller类
@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); } }
dto类
@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; } }
entity类
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; }
filter类
@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); } }
handler类
@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实现类
@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("注册成功"); } }
utils类
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; } }
到此这篇关于SpringSecurity+Redis+Jwt实现用户认证授权的文章就介绍到这了,更多相关SpringSecurity+Redis+Jwt认证授权内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!