java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > SpringSecurity+Redis+Jwt认证授权

SpringSecurity+Redis+Jwt实现用户认证授权

作者:MoCrane

SpringSecurity是一个强大且灵活的身份验证和访问控制框架,本文主要介绍了SpringSecurity+Redis+Jwt实现用户认证授权,具有一定的参考价值,感兴趣的可以了解一下

介绍

Spring Security是一个强大且灵活的身份验证和访问控制框架,用于Java应用程序。它是基于Spring框架的一个子项目,旨在为应用程序提供安全性。

Spring Security致力于为Java应用程序提供认证授权功能。开发者可以轻松地为应用程序添加强大的安全性,以满足各种复杂的安全需求。

SpringSecurity完整流程

一般认证工作流程

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("注册成功");
    }

登录流程

配置匿名访问

在配置类中允许登录请求可以匿名访问

调用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认证授权内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家! 

您可能感兴趣的文章:
阅读全文