石一歌的SpringSecurity笔记

SpringSecurity

Spring security 的核心功能主要包括:

  • 认证

  • 授权

  • 攻击防护 (防止伪造身份)

image-20220807224407816

其核心就是一组过滤器链,项目启动后将会自动配置。

  • UsernamePasswordAuthenticationFilter 用户名密码过滤
  • BasicAuthenticationFilter 基础过滤
  • ExceptionTranslationFilter 异常过滤(捕获抛出的错误,然后根据不同的认证方式进行信息的返回提示)
  • FilterSecurityInterceptor 过滤安全拦截器(判定该请求是否能进行访问rest服务)

注意:绿色的过滤器可以配置是否生效,其他的都不能控制。

原理

Spring Securty的核心类是:

  • WebSecurityConfigurerAdapter:自定义授权策略
  • AuthenticationManagerBuilder:自定义认证策略
  • @EnableWebSecurity:开启WebSecurity模式

工作流程

165977328379065

解释

  • @EnableWebSecurity
    • 在 Spring boot 应用中使用 Spring Security,用到了 @EnableWebSecurity注解,官方说明为,该注解和 @Configuration 注解一起使用, 注解 WebSecurityConfigurer 类型的类,或者利用@EnableWebSecurity 注解继承 WebSecurityConfigurerAdapter的类,这样就构成了 Spring Security 的配置。
  • WebSecurityConfigurerAdapter
    • 一般情况,会选择继承 WebSecurityConfigurerAdapter 类,其官方说明为:WebSecurityConfigurerAdapter 提供了一种便利的方式去创建 WebSecurityConfigurer的实例,只需要重写 WebSecurityConfigurerAdapter 的方法,即可配置拦截什么URL、设置什么权限等安全控制

认证

核心方法

	@Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(myUserDetailsService).passwordEncoder(passwordEncoder());;
    }

这个方法主要是配置验证数据库的用户名和密码信息并发挥特定格式的UserDetail子类,方便Security进行后续权限判断.
myUserDetailsService是我们定义的数据库验证类,主要结构如下:

@Service
@Slf4j
public class MyUserDetailsService implements UserDetailsService {
    /**
     * 通过用户名从数据库获取用户信息,添加用户拥有的多个角色
     * @param username
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //业务逻辑
        return adminUser;
    }
}

AdminUser定义的结构如下:

@Data
@EqualsAndHashCode(callSuper = false)
public class AdminUser implements UserDetails {
      各类常见的属性,是否锁定,是否授权过期等
      //用户权限
      private List<GrantedAuthority> authorities;
}

passwordEncoder()的加密方式的代码实现:

   /**
     * SpringSecurity5.X要求必须指定密码加密方式,否则会在请求认证的时候报错
     * 同样的,如果指定了加密方式,就必须您的密码在数据库中存储的是加密后的,才能比对成功
     */
    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

自定义认证过滤器

继承UsernamePasswordAuthenticationFilter,并添加到过滤器链

鉴权

 	@Override
    protected void configure(HttpSecurity http) throws Exception {
        /**
         * 1. HttpSecurity被声明为链式调用
         * 其中配置方法包括
         *  1. authorizeRequests()url拦截配置
         *  2. formLogin()表单验证
         *  3. httpBasic()表单验证
         *  4. csrf()提供的跨站请求伪造防护功能,一般直接关闭
         * 2. authorizeRequests目的是指定url进行拦截的,也就是默认这个url是“/”也就是所有的
         * anyanyRequest()、antMatchers()和regexMatchers()三种方法来拼配系统的url。并指定安全策略
         */
        //指定接口地址的权限 ANT模式的URL匹配器 
        http.authorizeRequests()
            .antMatchers("/product/**").hasAuthority("ADMIN")
            .antMatchers("/admin/**").hasAuthority("SUPERADMIN")
            .antMatchers("/login",  "/user/**", "/pub/**").permitAll();
        http.authorizeRequests()
            .anyRequest()
            .authenticated();
        http.exceptionHandling()
            .authenticationEntryPoint(authenticationEntryPoint)
            .accessDeniedHandler(accessDeniedHandler);
        //启动跨域,防止前端多个域名的情况
        http.cors();
        //禁止跨站请求伪造
        http.csrf().disable();
    	//注销的地址
    	http.logout().logoutUrl("/user/logout").logoutSuccessHandler(logoutSuccessHandler);
        //禁用session,基于token
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        //添加自定义的过滤器
        //先校验token,后发放token
        http.addFilterAt(jsonUsernamePasswordFilter(), UsernamePasswordAuthenticationFilter.class);
        http.addFilterBefore(tokenFilter, JsonUsernamePasswordFilter.class);
    }

JsonUsernamePasswordFilter()配置

    @Bean
    public JsonUsernamePasswordFilter jsonUsernamePasswordFilter() throws Exception {
        JsonUsernamePasswordFilter filter = new JsonUsernamePasswordFilter();
        //认证成功
        filter.setAuthenticationSuccessHandler(authenticationSuccessHandler);  
        //认证失败
        filter.setAuthenticationFailureHandler(authenticationFailureHandler);
        //目标url
        filter.setFilterProcessesUrl("/user/login");
        //重用WebSecurityConfigurerAdapter配置的AuthenticationManager
        filter.setAuthenticationManager(authenticationManagerBean());
        return filter;
    }

JsonUsernamePasswordFilter()处理逻辑

public class JsonUsernamePasswordFilter extends UsernamePasswordAuthenticationFilter {
    private boolean postOnly = true;
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException 	{
        if (postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException(
                    "Authentication method not supported: " + request.getMethod());
        }
        if (request.getContentType().startsWith(MediaType.APPLICATION_JSON_VALUE)) {
            UsernamePasswordAuthenticationToken authRequest;
            try (InputStream is = request.getInputStream()) {
                String body = IoUtil.read(is, CharsetUtil.CHARSET_UTF_8);
                JSONObject jsonObject = JSON.parseObject(body);
                String username = jsonObject.getJSONObject("data").getString("username");
                String password = jsonObject.getJSONObject("data").getString("password");
                // 校验IP地域 
                String requestsIp = IPUtil.getIpAddress(request);
                if (IpCheckUtil.checkIp(ipTableService.getList(), username, requestsIp) == false) {
                    ErrorMessage error = ErrorMessage.UN_USERNAME_STATUS;
                    error.setMessage("登录账号ip区域不允许, 您的IP:"+requestsIp);
                    throw BaseAuthenticationException.error(error);
                }
                authRequest = new UsernamePasswordAuthenticationToken(username, password);
                setDetails(request, authRequest);
                return this.getAuthenticationManager().authenticate(authRequest);
            } catch (IOException e) {
                e.printStackTrace();
                throw BaseAuthenticationException.error(ErrorMessage.LOGIN_ERROR);
            }
        } else {
            throw BaseAuthenticationException.error(ErrorMessage.NO_JSON);
        }
    }
}

TokenFilter

@Component
public class TokenFilter extends OncePerRequestFilter {
    private static final String TOKEN_KEY = "token";
    @Autowired
    private AdminTokenService tokenService;
    @Autowired
    private MyUserDetailsService userDetailsService;
    private static final Long MINUTES_30 = 30 * 60 * 1000L;
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        跟Filter类似的逻辑,根据http请求的header数据中的token进行验证并组装对象,认证失败不做相应处理,Security自己会做失败处理
            String token = getToken(request);
        if (StringUtils.isNotBlank(token)) {
            AdminUser loginUser = tokenService.getLoginUser(token);
            if (loginUser != null) {
                loginUser = checkLoginTime(loginUser);
                UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(loginUser,
                        null, loginUser.getAuthorities());
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        }
        filterChain.doFilter(request, response);
    }
}

复杂结构数据表如下:

165977723594543

自定义权限过滤器

继承了 SecurityMetadataSource(权限资源接口),过滤所有请求,核查这个请求需要的访问权限;主要实现Collection getAttributes(Object o)方法,此方法中可编写用户逻辑,根据用户预先设定的用户权限列表,返回访问此url需要的权限列表。相关代码如下:

@Component
public class MyFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
    @Autowired
    private MenuService menuService;
    private AntPathMatcher antPathMatcher = new AntPathMatcher();
    private static final Logger log = LoggerFactory.getLogger(MyFilterInvocationSecurityMetadataSource.class);
    @Override
    public Collection<ConfigAttribute> getAttributes(Object o) {
        String requestUrl = ((FilterInvocation) o).getRequestUrl();
        //去数据库查询资源
        List<Menu> allMenu = menuService.getAllMenu();
        for (Menu menu : allMenu) {
            if (antPathMatcher.match(menu.getUrl(), requestUrl)
                    && menu.getRoles().size() > 0) {
                List<Role> roles = menu.getRoles();
                int size = roles.size();
                String[] values = new String[size];
                for (int i = 0; i < size; i++) {
                    values[i] = roles.get(i).getName();
                }
                log.info("当前访问路径是{},这个url所需要的访问权限是{}", requestUrl, values);
                return SecurityConfig.createList(values);
            }
        }
        log.info("当前访问路径是{},这个url所需要的访问权限是{}", requestUrl, "ROLE_LOGIN");
        return SecurityConfig.createList("ROLE_LOGIN");
    }
    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }
    @Override
    public boolean supports(Class<?> aClass) {
        return FilterInvocation.class.isAssignableFrom(aClass);
    }
}

自定义权限决策管理器

实现AccessDecisionManager 的 void decide(Authentication auth, Object object, Collection cas) 方法,在上面的过滤器中,我们已经得到了访问此url需要的权限;那么,decide方法,先查询此用户当前拥有的权限,然后与上面过滤器核查出来的权限列表作对比,以此判断此用户是否具有这个访问权限,决定去留!所以顾名思义为权限决策器。

@Component
public class MyAccessDecisionManager implements AccessDecisionManager {
    @Override
    public void decide(Authentication auth, Object object, Collection<ConfigAttribute> cas) {
        Iterator<ConfigAttribute> iterator = cas.iterator();
        while (iterator.hasNext()) {
            if (auth == null) {
                throw new AccessDeniedException("当前访问没有权限");
            }
            ConfigAttribute ca = iterator.next();
            //当前请求需要的权限
            String needRole = ca.getAttribute();
            if ("ROLE_LOGIN".equals(needRole)) {
                if (auth instanceof AnonymousAuthenticationToken) {
                    throw new BadCredentialsException("未登录");
                } else
                    return;
            }
            //当前用户所具有的权限
            Collection<? extends GrantedAuthority> authorities = auth.getAuthorities();
            for (GrantedAuthority authority : authorities) {
                if (authority.getAuthority().equals(needRole)) {
                    return;
                }
            }
        }
        throw new AccessDeniedException("权限不足!");
    }
    @Override
    public boolean supports(ConfigAttribute configAttribute) {
        return true;
    }
    @Override
    public boolean supports(Class<?> aClass) {
        return true;
    }
}

然后在主方法中修改如下:

    @Autowired
    private MyFilterInvocationSecurityMetadataSource filterMetadataSource; //权限过滤器(当前url所需要的访问权限)
    @Autowired
    private MyAccessDecisionManager myAccessDecisionManager;//权限决策器
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                    @Override
                    public <O extends FilterSecurityInterceptor> O postProcess(O o) {
                        o.setSecurityMetadataSource(filterMetadataSource);
                        o.setAccessDecisionManager(myAccessDecisionManager);
                        return o;
                    }
                });
	.......
}

静态资源

就是用于配置静态资源的处理方式,可使用 Ant 匹配规则。就是可以不用认证就可以直接访问的接口

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/static","/static/**")
                .antMatchers("/commons");
    }

总结

WebSecurityConfigurerAdapter主要的3个方法

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    /**
     * 配置认证, 向security提供真实的用户身份。
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    }
    /**
     * 配置Security的认证策略, 每个模块配置使用 and() 结尾。这个也是最复杂的
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
    }
    /**
     * 配置静态资源的处理方式,可使用 Ant 匹配规则。就是可以不用认证就可以直接访问的接口
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
    }
}

165977924676330

springboot整合springsecurity

pom

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

UserServiceImpl

数据库读取用户信息进行身份认证,实现UserDetailService接口重写loadUserByUsername方法

@Service("userService")
public class UserServiceImpl implements UserDetailsService {
    @Autowired
    private UserService userService;
    
    /**
     * 需新建配置类注册一个指定的加密方式Bean,或在下一步Security配置类中注册指定
     */
    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 通过用户名从数据库获取用户信息
        User user = userService.getUser(username);
        if (user == null) {
            throw new UsernameNotFoundException("用户不存在");
        }

        // 添加用户拥有的多个角色
        List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
        Set<Role> roles = user.getRoles();
        for (Role role : roles) {
            grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_" + role.getRole()));
        }

        return new User(
                user.getUsername(),
             	//数据库存放密码应当加密,故此处直接使用
                user.getPassword(),
                //passwordEncoder.encode(user.getPassword()),
                authorities
        );
    }
}

SecurityConfig

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) // 开启方法级安全验证
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private UserService userService;

    /**
     * 指定加密方式 BCrypt
     */
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
	// 认证
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
         auth
                 // 从数据库读取的用户进行身份认证
                .userDetailsService(userService)
                .passwordEncoder(passwordEncoder());
    }
    // 授权
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .antMatchers(HttpMethod.POST, "/add-user").permitAll() // 允许post请求/add-user,而无需认证
            .anyRequest().authenticated()// 所有请求都需要验证						
            .and()
		   .formLogin().loginPage("/toLogin")// 设置登录页及其参数
            .usernameParameter("username")
            .passwordParameter("password")
            .loginProcessingUrl("/login")
            .and()
            .logout().logoutSuccessUrl("/")// 注销
            .and()
            .rememberMe().rememberMeParameter("remember")// 记住我
            .and()
            .csrf().disable();// post请求要关闭csrf验证,不然访问报错;实际开发中开启,需要前端配合传递其他参数
    }
}

controller

开启方法的权限注解后,配合@PreAuthorize(),限定方法访问权限

 @PreAuthorize("hasAnyRole('user')") 
 public int updatePwd(String oldPwd, String newPwd) {
        // 获取当前登录用户信息(注意:没有密码的)
        UserDetails principal = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        String username = principal.getUsername();

        // 通过用户名获取到用户信息(获取密码)
        UserInfo userInfo = userInfoMapper.getUserInfoByUsername(username);

        // 判断输入的旧密码是正确
        if (passwordEncoder.matches(oldPwd, userInfo.getPassword())) {
            // 不要忘记加密新密码
            return userInfoMapper.updatePwd(username, passwordEncoder.encode(newPwd));
        }
        return 0;
    }

springboot整合springsecurity+jwt

使用jwt的同时还使用了rea加密算法

pom

	   <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
	   <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-api</artifactId>
            <version>0.10.7</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-impl</artifactId>
            <version>0.10.7</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-jackson</artifactId>
            <version>0.10.7</version>
            <scope>runtime</scope>
        </dependency>

yaml

rsa:
  key:
    pubKeyPath: C:\Users\robod\Desktop\auth_key\id_key_rsa.pub
    priKeyPath: C:\Users\robod\Desktop\auth_key\id_key_rsa

RsaKeyProperties

@Data
@ConfigurationProperties("rsa.key")     //指定配置文件的key
public class RsaKeyProperties {

    private String pubKeyPath;

    private String priKeyPath;

    private PublicKey publicKey;
    private PrivateKey privateKey;

    @PostConstruct
    public void createKey() throws Exception {
        this.publicKey = RsaUtils.getPublicKey(pubKeyPath);
        this.privateKey = RsaUtils.getPrivateKey(priKeyPath);
    }
}

UserServiceImpl

这里只实现简单的查找用户功能,把认证全部转移到了我们自定义的过滤器中

@Service("userService")
public class UserServiceImpl implements UserDetailsService{
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        SysUser sysUser = userMapper.findByUsername(username);
        return sysUser;
    }
}

RsaUtils

public class RsaUtils {

    private static final int DEFAULT_KEY_SIZE = 2048;
    /**
     * 从文件中读取公钥
     *
     * @param filename 公钥保存路径,相对于classpath
     * @return 公钥对象
     * @throws Exception
     */
    public static PublicKey getPublicKey(String filename) throws Exception {
        byte[] bytes = readFile(filename);
        return getPublicKey(bytes);
    }

    /**
     * 从文件中读取密钥
     *
     * @param filename 私钥保存路径,相对于classpath
     * @return 私钥对象
     * @throws Exception
     */
    public static PrivateKey getPrivateKey(String filename) throws Exception {
        byte[] bytes = readFile(filename);
        return getPrivateKey(bytes);
    }

    /**
     * 获取公钥
     *
     * @param bytes 公钥的字节形式
     * @return
     * @throws Exception
     */
    private static PublicKey getPublicKey(byte[] bytes) throws Exception {
        bytes = Base64.getDecoder().decode(bytes);
        X509EncodedKeySpec spec = new X509EncodedKeySpec(bytes);
        KeyFactory factory = KeyFactory.getInstance("RSA");
        return factory.generatePublic(spec);
    }

    /**
     * 获取密钥
     *
     * @param bytes 私钥的字节形式
     * @return
     * @throws Exception
     */
    private static PrivateKey getPrivateKey(byte[] bytes) throws NoSuchAlgorithmException, InvalidKeySpecException {
        bytes = Base64.getDecoder().decode(bytes);
        PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(bytes);
        KeyFactory factory = KeyFactory.getInstance("RSA");
        return factory.generatePrivate(spec);
    }

    /**
     * 根据密文,生存rsa公钥和私钥,并写入指定文件
     *
     * @param publicKeyFilename  公钥文件路径
     * @param privateKeyFilename 私钥文件路径
     * @param secret             生成密钥的密文
     */
    public static void generateKey(String publicKeyFilename, String privateKeyFilename, String secret, int keySize) throws Exception {
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
        SecureRandom secureRandom = new SecureRandom(secret.getBytes());
        keyPairGenerator.initialize(Math.max(keySize, DEFAULT_KEY_SIZE), secureRandom);
        KeyPair keyPair = keyPairGenerator.genKeyPair();
        // 获取公钥并写出
        byte[] publicKeyBytes = keyPair.getPublic().getEncoded();
        publicKeyBytes = Base64.getEncoder().encode(publicKeyBytes);
        writeFile(publicKeyFilename, publicKeyBytes);
        // 获取私钥并写出
        byte[] privateKeyBytes = keyPair.getPrivate().getEncoded();
        privateKeyBytes = Base64.getEncoder().encode(privateKeyBytes);
        writeFile(privateKeyFilename, privateKeyBytes);
    }

    private static byte[] readFile(String fileName) throws Exception {
        return Files.readAllBytes(new File(fileName).toPath());
    }

    private static void writeFile(String destPath, byte[] bytes) throws IOException {
        File dest = new File(destPath);
        if (!dest.exists()) {
            dest.createNewFile();
        }
        Files.write(dest.toPath(), bytes);
    }
}

JwtUtils

使用了jwt框架jjwt

public class JwtUtils {

    private static final String JWT_PAYLOAD_USER_KEY = "user";

    /**
     * 私钥加密token
     *
     * @param userInfo   载荷中的数据
     * @param privateKey 私钥
     * @param expire     过期时间,单位分钟
     * @return JWT
     */
    public static String generateTokenExpireInMinutes(Object userInfo, PrivateKey privateKey, int expire) {
        return Jwts.builder()
                .claim(JWT_PAYLOAD_USER_KEY, JsonUtils.toString(userInfo))
                .setId(createJTI())
                .setExpiration(DateTime.now().plusMinutes(expire).toDate())
                .signWith(privateKey, SignatureAlgorithm.RS256)
                .compact();
    }

    /**
     * 私钥加密token
     *
     * @param userInfo   载荷中的数据
     * @param privateKey 私钥
     * @param expire     过期时间,单位秒
     * @return JWT
     */
    public static String generateTokenExpireInSeconds(Object userInfo, PrivateKey privateKey, int expire) {
        return Jwts.builder()
                .claim(JWT_PAYLOAD_USER_KEY, JsonUtils.toString(userInfo))
                .setId(createJTI())
                .setExpiration(DateTime.now().plusSeconds(expire).toDate())
                .signWith(privateKey, SignatureAlgorithm.RS256)
                .compact();
    }

    /**
     * 公钥解析token
     *
     * @param token     用户请求中的token
     * @param publicKey 公钥
     * @return Jws<Claims>
     */
    private static Jws<Claims> parserToken(String token, PublicKey publicKey) {
        return Jwts.parser().setSigningKey(publicKey).parseClaimsJws(token);
    }

    private static String createJTI() {
        return new String(Base64.getEncoder().encode(UUID.randomUUID().toString().getBytes()));
    }

    /**
     * 获取token中的用户信息
     *
     * @param token     用户请求中的令牌
     * @param publicKey 公钥
     * @return 用户信息
     */
    public static <T> Payload<T> getInfoFromToken(String token, PublicKey publicKey, Class<T> userType) {
        Jws<Claims> claimsJws = parserToken(token, publicKey);
        Claims body = claimsJws.getBody();
        Payload<T> claims = new Payload<>();
        claims.setId(body.getId());
        claims.setUserInfo(JsonUtils.toBean(body.get(JWT_PAYLOAD_USER_KEY).toString(), userType));
        claims.setExpiration(body.getExpiration());
        return claims;
    }

    /**
     * 获取token中的载荷信息
     *
     * @param token     用户请求中的令牌
     * @param publicKey 公钥
     * @return 用户信息
     */
    public static <T> Payload<T> getInfoFromToken(String token, PublicKey publicKey) {
        Jws<Claims> claimsJws = parserToken(token, publicKey);
        Claims body = claimsJws.getBody();
        Payload<T> claims = new Payload<>();
        claims.setId(body.getId());
        claims.setExpiration(body.getExpiration());
        return claims;
    }
}

SysUser

@Data
public class SysUser implements UserDetails {

    private Integer id;
    private String username;
    private String password;
    private Integer status;
    private List<SysRole> roles = new ArrayList<>();	//SysRole封装了角色信息,和登录无关,我放在后面讲

	//这里还有几个UserDetails中的方法,我就不贴代码了

}

SysRole

@Data
public class SysRole implements GrantedAuthority {

    private Integer id;
    private String roleName;
    private String roleDesc;

    /**
     * 如果授予的权限可以当作一个String的话,就可以返回一个String
     * @return
     */
    @JsonIgnore
    @Override
    public String getAuthority() {
        return roleName;
    }

}

JwtLoginFilter

public class JwtLoginFilter extends UsernamePasswordAuthenticationFilter {

    private AuthenticationManager authenticationManager;
    private RsaKeyProperties rsaKeyProperties;

    public JwtLoginFilter(AuthenticationManager authenticationManager, RsaKeyProperties rsaKeyProperties) {
        this.authenticationManager = authenticationManager;
        this.rsaKeyProperties = rsaKeyProperties;
    }

    //这个方法是用来去尝试验证用户的
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        try {
            SysUser user = JSONObject.parseObject(request.getInputStream(),SysUser.class);
            return authenticationManager.authenticate(
                    new UsernamePasswordAuthenticationToken(
                           user.getUsername(),
                           user.getPassword())
            );
        } catch (Exception e) {
            try {
                response.setContentType("application/json;charset=utf-8");
                response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                PrintWriter out = response.getWriter();
                Map<String, Object> map = new HashMap<>();
                map.put("code", HttpServletResponse.SC_UNAUTHORIZED);
                map.put("message", "账号或密码错误!");
                out.write(new ObjectMapper().writeValueAsString(map));
                out.flush();
                out.close();
            } catch (Exception e1) {
                e1.printStackTrace();
            }
            throw new RuntimeException(e);
        }
    }

    //成功之后执行的方法
    @Override
    public void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        SysUser sysUser = new SysUser();
        sysUser.setUsername(authResult.getName());
        sysUser.setRoles((List<SysRole>) authResult.getAuthorities());
        String token = JwtUtils.generateTokenExpireInMinutes(sysUser,rsaKeyProperties.getPrivateKey(),24*60);
        response.addHeader("Authorization", "RobodToken " + token);	//将Token信息返回给用户
        try {
            //登录成功时,返回json格式进行提示
            response.setContentType("application/json;charset=utf-8");
            response.setStatus(HttpServletResponse.SC_OK);
            PrintWriter out = response.getWriter();
            Map<String, Object> map = new HashMap<String, Object>(4);
            map.put("code", HttpServletResponse.SC_OK);
            map.put("message", "登陆成功!");
            out.write(new ObjectMapper().writeValueAsString(map));
            out.flush();
            out.close();
        } catch (Exception e1) {
            e1.printStackTrace();
        }
    }
}

JwtVerifyFilter

public class JwtVerifyFilter extends BasicAuthenticationFilter {

    private RsaKeyProperties rsaKeyProperties;

    public JwtVerifyFilter(AuthenticationManager authenticationManager, RsaKeyProperties rsaKeyProperties) {
        super(authenticationManager);
        this.rsaKeyProperties = rsaKeyProperties;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        String header = request.getHeader("Authorization");

        //没有登录
        if (header == null || !header.startsWith("RobodToken ")) {
            chain.doFilter(request, response);
            response.setContentType("application/json;charset=utf-8");
            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
            PrintWriter out = response.getWriter();
            Map<String, Object> map = new HashMap<String, Object>(4);
            map.put("code", HttpServletResponse.SC_FORBIDDEN);
            map.put("message", "请登录!");
            out.write(new ObjectMapper().writeValueAsString(map));
            out.flush();
            out.close();
            return;
        }
        //登录之后从token中获取用户信息
        String token = header.replace("RobodToken ","");
        SysUser sysUser = JwtUtils.getInfoFromToken(token, rsaKeyProperties.getPublicKey(), SysUser.class).getUserInfo();
        if (sysUser != null) {
            Authentication authResult = new UsernamePasswordAuthenticationToken
                    (sysUser.getUsername(),null,sysUser.getAuthorities());
            SecurityContextHolder.getContext().setAuthentication(authResult);
            chain.doFilter(request, response);
        }
    }
}

SecurityConfig

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true)// 开启权限控制的注解支持
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    //认证
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userService).passwordEncoder(passwordEncoder());
    }

    //授权
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()  //关闭csrf
            	.authorizeRequests()
                .antMatchers("/**").hasAnyRole("USER") //角色信息
                .anyRequest()   //其它资源
                .authenticated()    //表示其它资源认证通过后
                .and()
                .addFilter(new JwtLoginFilter(super.authenticationManager(),rsaKeyProperties))
                .addFilter(new JwtVerifyFilter(super.authenticationManager(),rsaKeyProperties))
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);    //禁用session
    }
}

controller

开启权限注解后,配合@Secured,限定方法访问权限

 	@Secured("ROLE_PRODUCT")
    @RequestMapping("/findAll")
    public String findAll() {
        return "产品列表查询成功";
    }

SpringBootApplication

加入res配置

@SpringBootApplication
@EnableConfigurationProperties(RsaKeyProperties.class)  //将配置类放入Spring容器中
public class SecurityApplication {
    public static void main(String[] args) {
        SpringApplication.run(ResourceApplication.class, args);
    }
}

@EnableGlobalMethodSecurity

该注解有(jsr250Enabled = true, prePostEnabled = true, securedEnabled = true)三种机制,结合实际情况使用

QQ截图20220103191856

springboot整合springsecurity+jwt(common-security)

具体应用参考官方文档

总结

  • 认证

    认证过程:UsernamePasswordAuthenticationFilter->SecurityConfig.configure(AuthenticationManagerBuilder auth)->UserDetailsService.loadUserByUsername()->UsernamePasswordAuthenticationFilter

    要想实现认证,我们可以自由的替换后三个地方,实现我们想要的功能。即直接写在security配置中、重写UserDetailsService.loadUserByUsername()方法和实现UsernamePasswordAuthenticationFilter来实现。

    • 认证用户实现UserDetails接口
    • UserService实现UserDetailsService接口,重写loadUserByUsername()方法,从数据库中获取数据
    • 实现自己的认证过滤器继承UsernamePasswordAuthenticationFilter,重写attemptAuthentication()和successfulAuthentication()方法,实现业务逻辑
    • Spring Security的配置类继承自WebSecurityConfigurerAdapter,重写里面的两个config()方法
  • 鉴权

    • 封装权限信息的类实现GrantedAuthority接口,并实现里面的getAuthority()方法
    • 实现自己的Token校验过滤器继承BasicAuthenticationFilter,并重写doFilterInternal()方法,实现业务逻辑
    • 编写Spring Security的配置类继承WebSecurityConfigurerAdapter,重写configure()方法添加自定义的过滤器,
    • 添加@EnableGlobalMethodSecurity(securedEnabled = true)注解开启注解权限控制的功能

参考链接

SpringBoot整合SpringSecurity(通俗易懂)

SpringBoot整合Spring Security【超详细教程】

@EnableGlobalMethodSecurity注解详解

SpringSecurity登录原理(源码级讲解)

common-security

posted @   Faetbwac  阅读(159)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
点击右上角即可分享
微信分享提示