利用Spring Security实现用户认证

目录


Spring Security认证步骤

Spring Security认证实现

添加Spring Security依赖

自定义UserDetails类

编写UserService接口

编写UserService接口实现类

自定义UserDetailsService类

编写自定义认证成功处理器

编写自定义认证失败处理器

编写认证用户无权限访问处理器

编写匿名用户访问资源处理器

编写Spring Security配置类

测试登录认证接口

认证成功返回token

什么是token?

token认证流程图

认证成功处理器返回token信息

(1)封装token返回的数据信息

(2)编写token工具类

(3)编写全局配置文件

(4)认证成功处理器类返回token数据

最终效果


 

Spring Security认证步骤

  1. 自定UserDetails类:当实体对象字段不满足时需要自定义UserDetails,一般都要自定义
    UserDetails。
  2. 自定义UserDetailsService类,主要用于从数据库查询用户信息。
  3. 创建登录认证成功处理器,认证成功后需要返回JSON数据,菜单权限等。
  4. 创建登录认证失败处理器,认证失败需要返回JSON数据,给前端判断。
  5. 创建匿名用户访问无权限资源时处理器,匿名用户访问时,需要提示JSON。
  6. 创建认证过的用户访问无权限资源时的处理器,无权限访问时,需要提示JSON。
  7. 配置Spring Security配置类,把上面自定义的处理器交给Spring Security。

Spring Security认证实现

添加Spring Security依赖

在pom.xml文件中添加Spring Security核心依赖,代码如下所示:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

自定义UserDetails类

当实体对象字段不满足时Spring Security认证时,需要自定义UserDetails。

  1. 将User类实现UserDetails接口
  2. 将原有的isAccountNonExpired、isAccountNonLocked、isCredentialsNonExpired和isEnabled属性修
    改成boolean类型,同时添加authorities属性。

注意:上述4个属性只能是非包装类的boolean类型属性,且默认值设置为true

@TableName(value = "user")
@Data
public class User implements Serializable, UserDetails {

    //省略原有的属性......

    /**
     * 帐户是否过期(1-未过期,0-已过期)
     */
    private boolean isAccountNonExpired = true;

    /**
     * 帐户是否被锁定(1-未过期,0-已过期)
     */
    private boolean isAccountNonLocked = true;

    /**
     * 密码是否过期(1-未过期,0-已过期)
     */
    private boolean isCredentialsNonExpired = true;

    /**
     * 帐户是否可用(1-可用,0-禁用)
     */
    private boolean isEnabled = true;

    @TableField(exist = false)
    private static final long serialVersionUID = 1L;

    /**
     * 权限列表
     */
    @TableField(exist = false)
    Collection<? extends GrantedAuthority> authorities;
    /**
     * 查询用户权限列表
     */
    @TableField(exist = false)
    private List<Permission> permissionList;
}

编写UserService接口

在 com.example.mybox.service.UserService 接口编写 根据用户名查询用户信息 的方法

/**
* @author Mr.Li
* @description 针对表【user】的数据库操作Service
* @createDate 2024-09-25 14:21:20
*/
public interface UserService extends IService<User> {
    /**
     * 根据用户名查询用户信息
     * @param username
     * @return
     */
    User findUserByUserName(String username);
}

编写UserService接口实现类

在 com.example.mybox.service.impl.UserServiceImpl 类中实现 UserService接口。

/**
 * @author Mr.Li
 * @description 针对表【user】的数据库操作Service实现
 * @createDate 2024-09-25 14:21:20
 */
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User>
        implements UserService {

    /**
     * 根据用户名查询用户信息
     *
     * @param username
     * @return
     */
    @Override
    public User findUserByUserName(String username) {
        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(User::getUsername, username);
        return baseMapper.selectOne(queryWrapper);
    }
}

自定义UserDetailsService类

在 com.example.mybox.config.security.service 包下创建CustomerUserDetailsService用户认证处理类,该类需要实现 UserDetailsService 接口。

/**
 * 用户认证处理器
 */
@Component
public class CustomerUserDetailsService implements UserDetailsService {

    @Resource
    private UserService userService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userService.findUserByUserName(username);
        //根据user是否存在判断认证情况
        if (user == null) {
            throw new UsernameNotFoundException("用户名或密码错误");
        }
        return user;
    }
}

编写自定义认证成功处理器

在 com.example.mybox.config.security.handler 包下创建 LoginSuccessHandler 登录认证成功处理器类。

/**
 * 登录认证成功处理器类
 */
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {
    @Resource
    private JwtUtils jwtUtils;

    @Resource
    private RedisService redisService;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request,
                                        HttpServletResponse response,
                                        Authentication authentication)
            throws IOException, ServletException {

        //设置客户端的响应的内容类型
        response.setContentType("application/json;charset=UTF-8");

        //获取当登录用户信息
        User user = (User) authentication.getPrincipal();

        //消除循环引用
        String result = JSON.toJSONString(loginResult, SerializerFeature.DisableCircularReferenceDetect);

        //获取输出流
        ServletOutputStream outputStream = response.getOutputStream();
        outputStream.write(result.getBytes(StandardCharsets.UTF_8));
        outputStream.flush();
        outputStream.close();
    }
}

编写自定义认证失败处理器

 com.example.mybox.config.security.handler 包下创建 LoginFailureHandler 登录认证失败处理器类。

/**
 * 登录认证失败处理器类
 */

@Component
public class LoginFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request,
                                        HttpServletResponse response,
                                        AuthenticationException exception)
            throws IOException, ServletException {

        //设置客户端响应编码格式
        response.setContentType("application/json;charset=UTF-8");

        //获取输出流
        ServletOutputStream outputStream = response.getOutputStream();
        String message = null;//提示信息

        int code = ResultCode.UNAUTHORIZED_CODE;//错误编码

        //判断异常类型
        if (exception instanceof AccountExpiredException) {
            message = "账户过期,登录失败!";
        } else if (exception instanceof BadCredentialsException) {
            message = "用户名或密码错误!";
        } else if (exception instanceof CredentialsExpiredException) {
            message = "密码过期,登录失败!";
        } else if (exception instanceof DisabledException) {
            message = "账户被禁用,登录失败!";
        } else if (exception instanceof LockedException) {
            message = "账户被锁,登录失败!";
        } else if (exception instanceof InternalAuthenticationServiceException) {
            message = "账户不存在,登录失败!";
        }else if (exception instanceof CustomerAuthenticationException) {
            message = exception.getMessage();
        } else {
            message = "登录失败!";
        }

        //将错误信息转换成JSON
        String result = JSON.toJSONString(Result.error(message, code));
        outputStream.write(result.getBytes(StandardCharsets.UTF_8));
        outputStream.flush();
        outputStream.close();
    }
}

编写认证用户无权限访问处理器

在 com.example.mybox.config.security.handler 包下创建 CustomerAccessDeniedHandler 认证用户访问无权限资源时处理器类。

/**
 * 访问无权限处理器
 */
@Component
public class CustomerAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response,
                       AccessDeniedException accessDeniedException)
            throws IOException, ServletException {

        //设置客户端的响应的内容类型
        response.setContentType("application/json;charset=UTF-8");

        //获取输出流
        ServletOutputStream outputStream = response.getOutputStream();

        //消除循环引用
        String result = JSON.toJSONString(
                Result.error("无权限,请联系管理员!", ResultCode.NOT_ALLOWED_CODE),
                SerializerFeature.DisableCircularReferenceDetect);
        outputStream.write(result.getBytes(StandardCharsets.UTF_8));
        outputStream.flush();
        outputStream.close();
    }
}

编写匿名用户访问资源处理器

在 com.example.mybox.config.security.handler 包下创建 AnonymousAuthenticationHandler 匿名用户访问资源处理器类。

/**
 * 匿名访问资源处理器
 */
@Component
public class AnonymousAuthenticationHandler implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response,
                         AuthenticationException authException)
            throws IOException, ServletException {

        //设置客户端的响应的内容类型
        response.setContentType("application/json;charset=UTF-8");

        //获取输出流
        ServletOutputStream outputStream = response.getOutputStream();

        //消除循环引用
        String result = JSON.toJSONString(Result.error("匿名无权限,请联系管理员!", ResultCode.NOT_ALLOWED_CODE),
                SerializerFeature.DisableCircularReferenceDetect);
                outputStream.write(result.getBytes(StandardCharsets.UTF_8));
        outputStream.flush();
        outputStream.close();
    }
}

编写Spring Security配置类

在 com.example.mybox.config.security 包下创建 SpringSecurityConfig 配置类。

@Configuration
@EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
    @Resource
    private CustomerUserDetailsService customerUserDetailsService;
    @Resource
    private LoginSuccessHandler loginSuccessHandler;
    @Resource
    private LoginFailureHandler loginFailureHandler;
    @Resource
    private AnonymousAuthenticationHandler anonymousAuthenticationHandler;
    @Resource
    private CustomerAccessDeniedHandler customerAccessDeniedHandler;


    /**
     * 注入加密类
     * @return
     */
    @Bean
    public BCryptPasswordEncoder passwordEncoder(){
       return new BCryptPasswordEncoder();
    }

    /**
     * 处理登录认证
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http.formLogin()
                .loginProcessingUrl("/user/login")
                // 设置登录验证成功或失败后的的跳转地址
                .successHandler(loginSuccessHandler)
                .failureHandler(loginFailureHandler)
                // 禁用csrf防御机制
                .and().csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                .antMatchers("/user/login").permitAll()
                .anyRequest().authenticated()
                .and()
                .exceptionHandling()
                .authenticationEntryPoint(anonymousAuthenticationHandler)
                .accessDeniedHandler(customerAccessDeniedHandler)
                .and().cors();//开启跨域配置
    }

    /**
     * 配置认证处理器
     *
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(customerUserDetailsService).passwordEncoder(passwordEncoder());
    }
}

测试登录认证接口

c672d9fe0bcc45a9b4ebdb6f9a423d67.png

认证成功返回token

什么是token?

Token是服务端生成的一串字符串,以作客户端进行请求的一个令牌,当第一次登录后,服务器生成一个Token便将此Token返回给客户端,以后客户端只需带上这个Token前来请求数据即可,无需再次带上用户名和密码。

token认证流程图

afd3857a57e34952a77f1ee0a0923ae6.png

认证成功处理器返回token信息

(1)封装token返回的数据信息

/**
 * 封装token返回的数据信息
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginResult {
    //用户名
    private String username;
    //状态码
    private int code;
    //token令牌
    private String token;
    //token过期时间
    private Long expireTime;
}

(2)编写token工具类

@Data
@ConfigurationProperties(prefix = "jwt")
@Component
public class JwtUtils {
    //密钥
    @Value("${jwt.secret}")
    private String secret;

    // 过期时间 毫秒
    @Value("${jwt.expiration}")
    private Long expiration;


    /**
     * 从数据声明生成令牌
     *
     * @param claims 数据声明
     * @return 令牌
     */
    private String generateToken(Map<String, Object> claims) {
        Date expirationDate = new Date(System.currentTimeMillis() + expiration);
        return Jwts.builder().setClaims(claims).setExpiration(expirationDate).signWith(SignatureAlgorithm.HS512, secret).compact();
    }

    /**
     * 从令牌中获取数据声明
     *
     * @param token 令牌
     * @return 数据声明
     */
    public Claims getClaimsFromToken(String token) {
        Claims claims;
        try {
            claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
        } catch (Exception e) {
            claims = null;
        }
        return claims;
    }

    /**
     * 生成令牌
     *
     * @param userDetails 用户
     * @return 令牌
     */
    public String generateToken(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>(2);
        claims.put(Claims.SUBJECT, userDetails.getUsername());
        claims.put(Claims.ISSUED_AT, new Date());
        return generateToken(claims);
    }

    /**
     * 从令牌中获取用户名
     *
     * @param token 令牌
     * @return 用户名
     */
    public String getUsernameFromToken(String token) {
        String username;
        try {
            Claims claims = getClaimsFromToken(token);
            username = claims.getSubject();
        } catch (Exception e) {
            username = null;
        }
        return username;
    }

    /**
     * 判断令牌是否过期
     *
     * @param token 令牌
     * @return 是否过期
     */
    public Boolean isTokenExpired(String token) {
        Claims claims = getClaimsFromToken(token);
        Date expiration = claims.getExpiration();
        return expiration.before(new Date());
    }

    /**
     * 刷新令牌
     *
     * @param token 原令牌
     * @return 新令牌
     */
    public String refreshToken(String token) {
        String refreshedToken;
        try {
            Claims claims = getClaimsFromToken(token);
            claims.put(Claims.ISSUED_AT, new Date());
            refreshedToken = generateToken(claims);
        } catch (Exception e) {
            refreshedToken = null;
        }
        return refreshedToken;
    }

    /**
     * 验证令牌
     *
     * @param token       令牌
     * @param userDetails 用户
     * @return 是否有效
     */
    public Boolean validateToken(String token, UserDetails userDetails) {
        User user = (User) userDetails;
        String username = getUsernameFromToken(token);
        return (username.equals(user.getUsername()) && !isTokenExpired(token));
    }
}

(3)编写全局配置文件

在 application.yml 全局配置文件中自定义jwt属性。

# jwt 配置
jwt:
  # 有效期1天(单位:s)
  expiration: 1800000
  # secret: 秘钥(普通字符串)
  secret: aHR0cHM6Ly9teS5vc2NoaW5hLm5ldC91LzM2ODE4Njg=

(4)认证成功处理器类返回token数据

在原有的 LoginSuccessHandler 登录认证成功处理器类上加入jwt相关代码。

/**
 * 登录认证成功处理器类
 */
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {
    @Resource
    private JwtUtils jwtUtils;

    @Resource
    private RedisService redisService;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request,
                                        HttpServletResponse response,
                                        Authentication authentication)
            throws IOException, ServletException {
        //设置客户端的响应的内容类型
        response.setContentType("application/json;charset=UTF-8");
        //获取当登录用户信息
        User user = (User) authentication.getPrincipal();
        //生成token
        String token = jwtUtils.generateToken(user);
        //设置token签名以及过期时间
        long jwt = Jwts.parser()
                .setSigningKey(jwtUtils.getSecret())
                .parseClaimsJws(token.replace("jwt_", ""))
                .getBody().getExpiration().getTime();
        LoginResult loginResult = new LoginResult(user.getUsername(), ResultCode.SUCCESS_CODE, token, jwt);

        //消除循环引用
        String result = JSON.toJSONString(loginResult, SerializerFeature.DisableCircularReferenceDetect);
        //获取输出流
        ServletOutputStream outputStream = response.getOutputStream();
        outputStream.write(result.getBytes(StandardCharsets.UTF_8));
        outputStream.flush();
        outputStream.close();

        //把生成的token存到redis
        String tokenKey = "token_"+token;
        redisService.set(tokenKey,token,jwtUtils.getExpiration() / 1000);
    }
}

最终效果

01d57992ba964a4ca523f593894dbadc.png

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

BergerLee

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值