认证授权

Jingxc大约 8 分钟java后端java后端Spring SecurityJWT

认证授权

1.完整流程

SpringSecurity的原理起始就是一个过滤器链,内部包含了各种功能的过滤器

  • WebAsyncManagerIntegrationFilter:将WebAsyncManger与SpringSecurity上下文进行集成
  • SecurityContextPersistenceFilter:在处理请求之前, 将安全信息加载到SecurityContextHolder中
  • HeaderWriterFilter:处理头信息假如响应中
  • CsrfFilter:处理CSRF攻击
  • LogoutFilter:处理注销登录
  • UsernamePasswordAuthenticationFilter,处理表单登录
  • DefaultLoginPageGeneratingFilter:配置默认登录页面
  • DefaultLogoutPageGeneratingFilter:配置默认注销页面
  • BasicAuthenticationFilter:处理HttpBasic登录
  • RequestCacheAwareFilter:处理请求缓存
  • SecurityContextHolderAwareRequestFilter:包装原始请求
  • AnonymousAuthenticationFilter:配置匿名认证
  • SessionManagementFilter:处理session并发问题
  • ExceptionTranslationFilter,异常处理
  • FilterSecurityInterceptor,授权
认证流程
认证流程

自己实现对SpringSecurity的流程做修改(登陆)

认证流程
认证流程

请求其他方法(认证)

认证流程
认证流程

思路分析

登陆:

  1. 自定义登陆接口
    • 调用ProviderManager的方法进行认证
  2. 自定义UserDetailsService
    • 实现中去查询数据库

校验:

  1. 定义JWT认证过滤器
    • 获取token
    • 解析token,从(缓存)获取用户信息

2.maven依赖

<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

3.用户登录

自定义登陆接口,让SpringSecurity对这个接口放行,让用户访问这个接口时不用登陆也能访问

在接口中通过AuthenticationManager的authenticate方法来进行用户认证,需要在SecurityConfig中配置把AuthenticationManager注入容器。

认证成功的话生成一个jwt,放入相应返回,并且为了让用户下次请求时能通过jwt识别出具体是哪个用户,需要把用户信息存入redis,可以把用户id作为key

3.1自定义UserDetailsService


自定义UserDetailsService,实现去数据库查询用户信息

  • 实现UserDetailsService接口
  • 重写loadUserByUsername方法
import javax.annotation.Resource;

import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.game.server.bean.SecurityUser;
import com.game.server.bean.UserInfo;
import com.game.server.exception.ReturnCode200Exception;
import com.game.server.mapper.UserInfoMapper;

@Service
public class UserDetailServiceImpl implements UserDetailsService {

    @Resource
    private UserInfoMapper userInfoMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        // 查询用户信息
        LambdaQueryWrapper<UserInfo> q = new LambdaQueryWrapper<>();
        UserInfo userInfo = userInfoMapper.selectOne(q.eq(UserInfo::getUsername, username));

        if (userInfo == null) {
            throw new ReturnCode200Exception("用户名或者密码错误");
        }

        // TODO 查询对应的权限信息

        //把数据封装成UserDetail类返回
        return new SecurityUser(userInfo);
    }

}

注意

这里需要返回一个UserDetails对象,我们可以自定义一个实体类

import java.util.Collection;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
public class SecurityUser implements UserDetails {

    private static final long serialVersionUID = 803802923747104863L;
    private UserInfo userInfo;
    private List<String> permissions;
    private List<SimpleGrantedAuthority> authorities;

    public SecurityUser(UserInfo userInfo, List<String> permissions) {
        super();
        this.userInfo = userInfo;
        this.permissions = permissions;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        // 将permissions中的权限信息进行封装
        if (authorities != null) {
            return authorities;
        }
        authorities = permissions.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
        return authorities;
    }

    @Override
    public String getPassword() {
        return userInfo.getPassword();
    }

    @Override
    public String getUsername() {
        return userInfo.getPassword();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }

}

3.2密码加密方式


通过BCryptPasswordEncoder方式进行密码加密,将PasswordEncoder注入spring容器管理

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    /**
        * 创建BCryptPasswordEncoder,注入容器,密码加密方式
        * 
        * @return
        */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

测试

public static void main(String[] args) {
    PasswordEncoder passwordEncoder1 = new BCryptPasswordEncoder();
    System.out.println(passwordEncoder1.encode("1234"));

    boolean matches = passwordEncoder1.matches("1234",
            "$2a$10$qh2ybkFoWPngVD1uk66FkOPCRKsb1rCk7ff75g2.XHFRBTkkL9klq");
    System.out.println(matches);
}

3.3用户登陆


  1. 自定义登陆接口
    • 调用ProviderManager的方法进行认证
@Service
public class LoginServiceImpl implements LoginService {

    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private RedisCacheUtil redisCacheUtil;

    @Override
    public ReturnResult login(UserInfo userInfo) {

        // 进行用户认证
        UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
                userInfo.getUsername(), userInfo.getPassword());
        Authentication authenticate = null;
        try {
            authenticate = authenticationManager.authenticate(authentication);
        } catch (AuthenticationException e) {
            throw new ReturnCode200Exception(e.getMessage());
        }

        SecurityUser securityUser = (SecurityUser) authenticate.getPrincipal();
        String id = securityUser.getUserInfo().getId();

        // 生成jwt-token,存储redis
        String token = JwtUtils.generateStringTokenExpireInMinutes(id, 60 * 12);
        securityUser.setAuthorities(null);
        redisCacheUtil.hmset("login-" + id, BeanMapUtils.entityToMapObject(securityUser));

        Map<String, String> res = new HashMap<>();
        res.put("toekn", token);
        return ReturnResultSuccess.builder().data(res).build();
    }
}

警告

上面需要从spring容器中拿到AuthenticationManager对象,通过authenticate进行用户认证

  • 需要将AuthenticationManager注入spring容器
  • 需要将登陆等接口进行过滤
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
    @Autowired
    private AuthenticationEntryPoint authenticationEntryPointImpl;
    @Autowired
    private AccessDeniedHandler accessDeniedHandlerImpl;

    /**
     * 创建BCryptPasswordEncoder,注入容器,密码加密方式
     * 
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 将AuthenticationManager注入容器
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()// 关闭csrf
                // 不通过session获取securityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                // 对于登陆接口,允许匿名访问
                .authorizeRequests().antMatchers("/user/login", "user/register")
                // 除上面接口外所有请求全部需要鉴权
                .anonymous().anyRequest().authenticated();

        // 添加过滤器
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

        // 添加异常处理器
        http.exceptionHandling()
                // 认证失败处理器
                .authenticationEntryPoint(authenticationEntryPointImpl)
                // 授权失败处理器
                .accessDeniedHandler(accessDeniedHandlerImpl);

        // 允许跨域
        http.cors();
    }
}

4.token认证过滤器

  1. 定义JWT认证过滤器
    • 获取token
    • 解析token,从(缓存)获取用户信息

4.1代码实现


@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Autowired
    private RedisCacheUtil redisCacheUtil;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        // 获取token
        String token = request.getHeader("token");
        if (StringUtils.isBlank(token)) {
            filterChain.doFilter(request, response);
            return;
        }

        // 解析token
        String id = "";
        try {
            Payload<Object> idFromToken = JwtUtils.getIdFromToken(token);
            id = idFromToken.getId();
        } catch (ExpiredJwtException e) {
            e.printStackTrace();
            throw new ReturnCode200Exception("token 非法");
        }
        // 从redis中获取用户信息
        String redisKey = "login-" + id;
        Map<Object, Object> user = redisCacheUtil.hmget(redisKey);
        if (user == null) {
            throw new ReturnCode200Exception("用户未登陆");
        }
        SecurityUser userInfo = BeanMapUtils.mapToEntity(user, SecurityUser.class);
        // 存入SecurityContextHolder
        // 获取权限信息封装到authentication
        UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userInfo, null,
                null);
        SecurityContextHolder.getContext().setAuthentication(authentication);
        filterChain.doFilter(request, response);

    }

}

4.2配置认证

将过滤器放在UsernamePasswordAuthenticationFilter过滤器之前

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    /**
     * 创建BCryptPasswordEncoder,注入容器,密码加密方式
     * 
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 将AuthenticationManager注入容器
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()// 关闭csrf
                // 不通过session获取securityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                // 对于登陆接口,允许匿名访问
                .authorizeRequests().antMatchers("/user/login", "user/register")
                // 除上面接口外所有请求全部需要鉴权
                .anonymous().anyRequest().authenticated();

        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
    }

}

5.用户登出

用户登出比较简单,前端携带token进行用户登出即可

@Override
@OperationLogger
public ReturnResult logout() {

    // 获取SecurityContextHolder中的用户id
    UsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken) SecurityContextHolder
            .getContext().getAuthentication();

    UserInfo userInfo = (UserInfo) authentication.getPrincipal();
    String id = userInfo.getId();
    // 清除redis信息
    redisCacheUtil.del("login-" + id);
    return ReturnResultSuccess.builder().build();

}

6.授权基本流程

在SpringSecurity中,会使用默认的FilterSecurityInterceptor来进行权限校验。在FilterSecurityInterceptor中会从SecurityContextHolder获取其中的Authentication,然后获取其中的权限信息,当前用户是否拥有访问当前资源所需的权限。

所以需要把当前用户的权限信息存入Authentication中,然后设置资源所需权限即可

6.1修改UserDetails实体类


在原有的实体类上增加成员变量,并修改权限方法

private List<String> permissions;
private List<SimpleGrantedAuthority> authorities;

public SecurityUser(UserInfo userInfo, List<String> permissions) {
    super();
    this.userInfo = userInfo;
    this.permissions = permissions;
}

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
    // 将permissions中的权限信息进行封装
    if (authorities != null) {
        return authorities;
    }
    authorities = permissions.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
    return authorities;
}

6.2在UserDetailsService实现类中赋值权限


@Service
public class UserDetailServiceImpl implements UserDetailsService {

    @Resource
    private UserInfoMapper userInfoMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        // 查询用户信息
        LambdaQueryWrapper<UserInfo> q = new LambdaQueryWrapper<>();
        UserInfo userInfo = userInfoMapper.selectOne(q.eq(UserInfo::getUsername, username));

        if (userInfo == null) {
            throw new ReturnCode200Exception("用户名或者密码错误");
        }

        // 查询对应的权限信息
        List<String> permissions = userInfoMapper.selectPermByUserId(userInfo.getId());

        return new SecurityUser(userInfo, permissions);
    }

}

6.3修改SecurityConfig配置类


增加@EnableGlobalMethodSecurity(prePostEnabled = true)注解

6.4修改JwtAuthenticationTokenFilter过滤器添加权限

// 获取权限信息封装到authentication
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userInfo, null,
        userInfo.getAuthorities());

7.自定义失败处理

在SpringSecurity中,如果我们认证或者授权过程中出现了异常会被ExceptionTranslationFilter捕获到,在ExceptionTranslationFilter中会去判断是认证失败还是授权失败出现的异常

如果是认证过程中出现的异常会被封装成AuthenticationException然后调用AuthenticationEntryPoint对象的方法去进行异常处理

如果是授权过程中出现的异常会被封装成AccessDeniedException然后调用AccessDeniedHandler对象去进行异常处理

所以自定义异常处理,只需要自定义AuthenticationEntryPoint和AccessDeniedHandler然后配置给SpringSecurity即可

@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response,
            AccessDeniedException accessDeniedException) throws IOException, ServletException {

        ReturnResult re = ReturnResultError.builder().msg("用户授权失败,请联系管理员查看权限信息").build();

        String json = JSON.toJSONString(re);

        response.setStatus(ConstantCommon.RETURN_CODE_200);
        response.setContentType("application/json");
        response.setCharacterEncoding("utf-8");
        response.getWriter().print(json);

    }

}


@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response,
            AuthenticationException authException) throws IOException, ServletException {
        ReturnResult re = ReturnResultError.builder().msg("用户认证失败,请登陆").build();

        String json = JSON.toJSONString(re);

        response.setStatus(ConstantCommon.RETURN_CODE_200);
        response.setContentType("application/json");
        response.setCharacterEncoding("utf-8");
        response.getWriter().print(json);

    }

}

修改配置文件

@Autowired
private AuthenticationEntryPoint authenticationEntryPointImpl;
@Autowired
private AccessDeniedHandler accessDeniedHandlerImpl;

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.csrf().disable()// 关闭csrf
            // 不通过session获取securityContext
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
            // 对于登陆接口,允许匿名访问
            .authorizeRequests().antMatchers("/user/login", "user/register")
            // 除上面接口外所有请求全部需要鉴权
            .anonymous().anyRequest().authenticated();

    // 添加过滤器
    http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

    // 添加异常处理器
    http.exceptionHandling()
            // 认证失败处理器
            .authenticationEntryPoint(authenticationEntryPointImpl)
            // 授权失败处理器
            .accessDeniedHandler(accessDeniedHandlerImpl);
}

8.跨域处理

8.1设置SpringBoot允许跨域

@Configuration
public class CorsConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {

        // 设置允许跨域路径
        registry.addMapping("/**")
                // 设置允许跨域请求域名
                .allowedOrigins("*")
                // 设置允许cookie
                .allowCredentials(true)
                // 设置允许请求方式
                .allowedMethods("GET", "POST", "DELETE", "PUT")
                // 是只允许header属性
                .allowedHeaders("*")
                // 跨域允许时间
                .maxAge(3600);

    }

}

8.2设置SpringSecurity允许跨域

在配置文件中增加http.cors();

上次编辑于:
贡献者: Jingxc