文章总结自三更草堂SpringSecurity框架教程,个人认为是B站最好用的Security+JWT讲解。
1. 项目搭建
1.1 新建SpringBoot项目
SpringBoot使用的是2.7.0版本
 依赖:
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>
创建一个HelloController:
@RestController
@RequestMapping("/hello")
public class HelloController {
    @GetMapping
    public String hello(){
        return "hello";
    }
}
访问测试:可以看到可以访问成功

1.2 引入SpringSecurity依赖
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
重启项目之后发现localhost:8088/hello接口无法再次进行访问,取而代之的是Security的登录页。

 此时的默认用户名是user,密码会在控制台输出。
2. 认证
2.1 登录流程

2.2 security认证流程
本质使用的是过滤器链,内部提供各种功能的过滤器。
- 
核心过滤器:

UsernamePasswordAuthenticationFilter:处理登录页中的登录请求;ExceptionTranslationFilter:处理过滤器链中的异常;FilterSecurityInterceptor:权限校验过滤器。
 - 
1.2案例认证流程:

Authentication:实现类,表示当前访问系统的用户,封装了相关用户信息;AuthenticationManager:定义认证Authentication方法;UserDetailsService:加载用户特定数据的核心接口;包含一个根据用户名查询用户信息的方法。UserDetails:提供核心用户信息,通过UserDetailsService根据用户名获取处理的用户信息封装成UserDetailsService对象返回,然后将信息封装到Authentication对象中。
 
2.3 实现思路
使用自己定义的接口去掉哟SpringSecurity中封装的类。
- 
登录流程:

 - 
校验过程:

 - 
引入redis:如果认证之后害需要通过JWT中的
userid对数据库进行查询,消耗太大,可以存储入到redis中。 
2.4 实现方案
- 登录:
- 自定义登录接口:调用
ProviderManager的方法进行认证;认证通过生成JWT;将用户信息存入redis; - 自定义
UserDetailsService:实现数据库查询。 
 - 自定义登录接口:调用
 - 校验:
- 定义JWT过滤器:获取token,解析token,获取userid,去redis中获取用户信息;将用户信息存入
SecurityContextHolder。 
 - 定义JWT过滤器:获取token,解析token,获取userid,去redis中获取用户信息;将用户信息存入
 
2.5 简单实现
- 创建数据库:
 
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user`  (
  `user_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '用户ID',
  `user_name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '用户名',
  `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '密码',
  `status` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '帐号状态(0正常 1停用)',
  `del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '删除标志(0代表存在 2代表删除)',
  PRIMARY KEY (`user_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 100 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户信息表' ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;
实体类:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class SysUser implements Serializable {
    @TableId
    private Long id;
    private String username;
    private String password;
    private Character status;
    private Integer delFlag;
}
- 引入数据库相关依赖:
 
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.2</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.38</version>
        </dependency>
- 配置mapper
 
@Component
public interface SysUserMapper extends BaseMapper<SysUser> {
}
添加注解扫描
@SpringBootApplication
@MapperScan("com.jm.springsecurity.mapper")
public class SpringSecurityApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringSecurityApplication.class, args);
    }
}
- 构建登录接口:实现
security中的类,自己实现登录逻辑UserDetailsService 
@Service
public class SysUserDetailServiceImpl implements UserDetailsService {
    @Autowired
    private SysUserMapper userMapper;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 查询用户信息
        QueryWrapper<SysUser> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("username",username);
        SysUser user = userMapper.selectOne(queryWrapper);
        if (Objects.isNull(user)){
            throw new RuntimeException("用户不存在");
        }
        return new LoginUser(user);
    }
}
- 封装登录返回实体类:实现
UserDetails接口自定义返回逻辑。 
@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginUser implements UserDetails {
    private SysUser user;
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;
    }
    @Override
    public String getPassword() {
        return user.getPassword();
    }
    @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;
    }
}
- 
登录测试: 登录失败,原因是
security拥有自己的密码校验,需要重写。

新建
SecurityConfig将BCryptPasswordEncoder注入到spring容器中,security就会自动使用来替换掉原有的密码校验方式。@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } }加密:针对密码进行加密之后,将加密后的密码存入数据库,登录时会自动走当前加密方式,不需要再做多余处理,即可登录成功。
@Autowired private PasswordEncoder passwordEncoder;System.out.println(passwordEncoder.encode("1234")); 
2.6 JWT使用
- 工具类
 
/**
 * JWT工具类
 */
public class JwtUtil {
    //有效期为
    public static final Long JWT_TTL = 60 * 60 *1000L;// 60 * 60 *1000  一个小时
    
    //设置秘钥明文
    public static final String JWT_KEY = "jmyy";
    public static String getUUID(){
        String token = UUID.randomUUID().toString().replaceAll("-", "");
        return token;
    }
    
    /**
     * 生成jtw
     * @param subject token中要存放的数据(json格式)
     * @return
     */
    public static String createJWT(String subject) {
        JwtBuilder builder = getJwtBuilder(subject, null, getUUID());// 设置过期时间
        return builder.compact();
    }
    /**
     * 生成jtw
     * @param subject token中要存放的数据(json格式)
     * @param ttlMillis token超时时间
     * @return
     */
    public static String createJWT(String subject, Long ttlMillis) {
        JwtBuilder builder = getJwtBuilder(subject, ttlMillis, getUUID());// 设置过期时间
        return builder.compact();
    }
    private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) {
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
        SecretKey secretKey = generalKey();
        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);
        if(ttlMillis==null){
            ttlMillis=JwtUtil.JWT_TTL;
        }
        long expMillis = nowMillis + ttlMillis;
        Date expDate = new Date(expMillis);
        return Jwts.builder()
                .setId(uuid)              //唯一的ID
                .setSubject(subject)   // 主题  可以是JSON数据
                .setIssuer("sg")     // 签发者
                .setIssuedAt(now)      // 签发时间
                .signWith(signatureAlgorithm, secretKey) //使用HS256对称加密算法签名, 第二个参数为秘钥
                .setExpiration(expDate);
    }
    /**
     * 创建token
     * @param id
     * @param subject
     * @param ttlMillis
     * @return
     */
    public static String createJWT(String id, String subject, Long ttlMillis) {
        JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id);// 设置过期时间
        return builder.compact();
    }
    public static void main(String[] args) throws Exception {
        // 加密
        String jwt = createJWT("2123");
        System.out.println(jwt);
        // 解密
        Claims claims = parseJWT("eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxMzU5Yzk3ZGFmNDE0MzEzOTMyZjYxMDZkNWYyNzg4YSIsInN1YiI6IjIxMjMiLCJpc3MiOiJzZyIsImlhdCI6MTY1NDQ5ODM4MCwiZXhwIjoxNjU0NTAxOTgwfQ.hAFpmr6u_AtlMEs9SqS9TT9yuzbdSDDNsuWMLWmKIgU");
        String subject = claims.getSubject();
        System.out.println(subject);
    }
    /**
     * 生成加密后的秘钥 secretKey
     * @return
     */
    public static SecretKey generalKey() {
        byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
        SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
        return key;
    }
    
    /**
     * 解析
     *
     * @param jwt
     * @return
     * @throws Exception
     */
    public static Claims parseJWT(String jwt) throws Exception {
        SecretKey secretKey = generalKey();
        return Jwts.parser()
                .setSigningKey(secretKey)
                .parseClaimsJws(jwt)
                .getBody();
    }
}
使用JWT工具类来实现加密和解密。
2.7 自定义登录接口
- 新增接口:
 
@RestController
public class LoginController {
    @Autowired
    private LoginService loginService;
    @PostMapping("/login")
    public Result login(@RequestBody SysUser user){
        return new Result(200,"登陆成功",loginService.login(user));
    }
}
- Security放行登录接口:
 
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //关闭csrf
                .csrf().disable()
                //不通过Session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 对于登录接口 允许匿名访问
                .antMatchers("/login").anonymous()
//                .antMatchers("/testCors").hasAuthority("system:dept:list222")
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated();
	}
- 配置redis:
 
spring:
  redis:
    host: localhost
    port: 6379
    database: 0
    password: 123456
- 
认证逻辑:
- 将
AuthenticationManager注入到容器中 
@Bean @Override protected AuthenticationManager authenticationManager() throws Exception { return super.authenticationManager(); }- 登录接口实现:
 
/** * @Description * @date 2022/6/6 15:02 */ public interface LoginService { TokenVO login(SysUser user); }@Service public class LoginServiceImpl implements LoginService { @Autowired private AuthenticationManager authenticationManager; @Autowired private RedisCache redisCache; @Override public TokenVO login(SysUser user) { // 认证 UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken( user.getUsername(), user.getPassword()); Authentication authenticate = authenticationManager.authenticate(authenticationToken); // 认证失败 if (Objects.isNull(authenticate)){ throw new RuntimeException("用户名或密码错误"); } // 认证成功 LoginUser loginUser = (LoginUser) authenticate.getPrincipal(); String userId = loginUser.getUser().getId().toString(); // 根据用户id生成jwt String jwt = JwtUtil.createJWT(userId); // 将用户信息存入redis redisCache.setCacheObject("login:"+userId,loginUser); return new TokenVO(jwt); } }- 返回vo
 
@Data @AllArgsConstructor public class TokenVO { private String token; } - 将
 
2.8 jwt过滤器
@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");
        if (StringUtils.hasText(token)) {
            //直接放行 让后面原生的 security 去拦截
            filterChain.doFilter(request, response);
            return;
        }
        // 解析token
        String userId;
        try {
            Claims claims = JwtUtil.parseJWT(token);
            userId = claims.getSubject();
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("token error");
        }
        // 从redis 获取用户信息
        String redisKey = "login:" + userId;
        LoginUser loginUser = redisCache.getCacheObject(redisKey);
        if (Objects.isNull(loginUser)){
            throw new RuntimeException("用户为登录");
        }
        // 将用户信息存入SecurityContextHolder
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(
                        loginUser,
                        null,
                        null);
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        // 放行
        filterChain.doFilter(request, response);
    }
}
- 配置jwt过滤器:将当前过滤器放到
security的UsernamePasswordAuthenticationFilter前面。修改SecurityConfig类 
	    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
	
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //关闭csrf
                .csrf().disable()
                //不通过Session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 对于登录接口 允许匿名访问
                .antMatchers("/login").anonymous()
//                .antMatchers("/testCors").hasAuthority("system:dept:list222")
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated();
        // 添加过滤器到某个过滤器前
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        }
2.9 退出登录
    @Override
    public Result logout() {
        // 获取SecurityContextHolder中用户id
        UsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        Long userId = loginUser.getUser().getId();
        // 删除redis中值
        redisCache.deleteObject("login:"+userId);
        return new Result(200,"注销成功");
    }
    @GetMapping("/logout")
    public Result logout(){
        return loginService.logout();
    }
相关文章
暂无评论...





