Spring Security+JWT实现身份认证与权限控制

2021SC@SDUSC

零. spring security鉴权流程简图

spring security流程

一. 用gradle/maven导入依赖(gradle)

首先导入以下依赖

// spring security
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-security', version: '2.5.5'
// jjwt
implementation group: 'io.jsonwebtoken', name: 'jjwt', version: '0.9.1'
// json
implementation group: 'com.google.code.gson', name: 'gson', version: '2.8.6'

二. 建立配置文件

整体配置文件

首先建立全局的Spring security的配置文件

// 开启spring security
@EnableWebSecurity
// 开启pre/post注解拦截
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        super.configure(auth);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //禁用session与csrf,这里打算采用token的方式所以不用session
        http.csrf().disable().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).disable();
        // 禁用form表单登录
        http.formLogin().disable();
        // 配置允许跨域
        http.cors();

        // 允许不经验证通过的接口
        http.authorizeRequests().antMatchers("/login", "/register").permitAll()
                // 其余接口均需要验证
                .anyRequest().authenticated();

        // option请求拦截器,cors请求如果是application/json之类的会首先做一次Options请求
        http.addFilterAfter(new OptionsRequestFilter(), CorsFilter.class)
                .apply(new UsernameLoginConfig<>()) // 应用用户名密码登录的config
                .and()
                .apply(new JwtLoginConfig<>()) //应用jwt登录的config
                .and().logout().disable(); //禁用默认的退出
    }

    /**
     * 配置cors跨域访问
     * @return cors配置
     */
    @Bean
    CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration().applyPermitDefaultValues();
        // 暴露请求头,这里的authentication头是jwt的规范之一
        configuration.addExposedHeader("Authorization");
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }

    /**
     * 密码编码器
     * @return BCryptPasswordEncoder
     */
    @Bean
    PasswordEncoder getPw() {
        return new BCryptPasswordEncoder();
    }
}

Options请求拦截器

配置Options请求拦截器OptionsRequestFilter

public class OptionsRequestFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        if (request.getMethod().equals("OPTIONS")) {
            // 允许通过的请求,这里仅允许get/post/head如果需要可以添加put/delete等等
            response.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,HEAD");
            response.setHeader("Access-Control-Allow-Headers", response.getHeader("Access-Control-Request-Headers"));
            return;
        }
        filterChain.doFilter(request, response);
    }

}

配置用户名登录拦截

spring security实际上有自己自带的UsernamePasswordAuthenticationFilter,我们在这儿就直接采用他的

Config&Filter

首先是配置UsernameLoginConfig.java配置文件

/**
 * 用户名登录的配置
 */
@Configuration
public class UsernameLoginConfig<T extends AbstractHttpConfigurer<T, B>, B extends HttpSecurityBuilder<B>>
        extends AbstractHttpConfigurer<T, B> {
    // 使用spring security自带的用户名密码校验
    // 默认的拦截/login的post请求,取出username password做校验
    private final UsernamePasswordAuthenticationFilter authFilter = new UsernamePasswordAuthenticationFilter();

    @Override
    public void configure(B builder) {
        authFilter.setPostOnly(true);
        // 获取系统默认的manager
        authFilter.setAuthenticationManager(builder.getSharedObject(AuthenticationManager.class));
        // 不使用session
        authFilter.setSessionAuthenticationStrategy(new NullAuthenticatedSessionStrategy());
        // 利用bean注入handler,可以交给spring控制生命周期,不用多次实例化
        authFilter.setAuthenticationFailureHandler(loginFailureHandler());
        authFilter.setAuthenticationSuccessHandler(usernameLoginSuccessHandler());
        UsernamePasswordAuthenticationFilter filter = postProcess(authFilter);
        //指定Filter的位置
        builder.addFilterAfter(filter, LogoutFilter.class);
    }

    @Bean
    LoginFailureHandler loginFailureHandler(){
        return new LoginFailureHandler();
    }

    @Bean
    UsernameLoginSuccessHandler usernameLoginSuccessHandler(){
        return new UsernameLoginSuccessHandler();
    }
}

handler

接着是配置UsernameLoginSuccessHandler,这个是用来处理登录成功后的流程,我们要做的是JWT的鉴权,因此在这个handler里应当生成jwt并给前端返回

public class UsernameLoginSuccessHandler implements AuthenticationSuccessHandler {
    private final static Gson gson = new Gson();

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                        Authentication authentication) throws IOException{
        //生成token,并放置在header里
        String token = JwtUtils.createToken(((BallUser)authentication.getPrincipal()).getUserId());
        Map.Entry<String, String> entry = JwtUtils.tokenToEntry(token);
        response.setHeader(entry.getKey(), entry.getValue());
        response.setCharacterEncoding("UTF-8");
        response.getWriter().print(gson.toJson(ResultEntity.data(token)));
    }

}

JwtUtil

这里放出来一个略微封装的jwt生成工具,可以根据项目自行定制

public class JwtUtils {
    public static final String TOKEN_HEADER = "Authorization"; //Header标识JWT
    private static final String TOKEN_PREFIX = "Bearer "; //JWT标准开头,注意空格
    private static final String SECRET = "jwt-key"; //JWT签证密钥
    private static final String ROLE = "role"; //Jwt中携带的身份key
    private static final String USER_ID = "uid"; //Jwt中携带的用户ID的key
    private static final long EXPIRATION = 60 * 60 * 24 * 7L; //过期时间7天

    /**
     * 从http header中map中检索token
     *
     * @param headers http的header
     * @return 如果检索到返回token否则返回null
     */
    public static String tokenFromHeaders(Map<String, String> headers) {
        if (headers == null) {
            return null;
        }
        for (Map.Entry<String, String> entry : headers.entrySet()) {
            if (entry.getKey().equals(TOKEN_HEADER)) {
                if (entry.getValue().startsWith(TOKEN_PREFIX)) {
                    return entry.getValue().replaceFirst(TOKEN_PREFIX, "");
                }
            }
        }
        return null;
    }

    public static String tokenFromHeader(String header) {
        if (header == null) {
            return null;
        }
        if (header.startsWith(TOKEN_PREFIX)) {
            return header.replaceFirst(TOKEN_PREFIX, "");
        }
        return null;
    }

    /**
     * 将token变成jwt标准的entry
     *
     * @param token token不能为null
     * @return 转换成的entry
     */
    public static Map.Entry<String, String> tokenToEntry(String token) {
        assert token != null;
        return new AbstractMap.SimpleEntry<String, String>(TOKEN_HEADER, TOKEN_PREFIX + token);
    }

    /**
     * 创建JWT
     *
     * @param userId 用户ID
     * @return 创建好的Token
     */
    public static String createToken(Integer userId) {
        Map<String, Object> map = new HashMap<>();
        map.put(USER_ID, userId);
        return Jwts.builder().signWith(SignatureAlgorithm.HS256, SECRET)
                .setSubject("info")
                .setIssuer("weiteli")
                .setAudience("together-ball")
                .setClaims(map).setIssuedAt(new Date())
                // token 7天内有效
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION * 1000))
                .compact();
    }

    /**
     * 重新刷新token有效期
     *
     * @param token 需要刷新的token
     * @return 已经刷新的token
     */
    public static String refreshToken(String token) {
        Claims claim = getTokenClaims(token);
        return Jwts.builder().signWith(SignatureAlgorithm.HS256, SECRET)
                .setClaims(claim).setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION * 1000))
                .compact();
    }

    /**
     * 根据token获取用户ID
     *
     * @param token JWT
     * @return 用户名
     */
    public static String getUsername(String token) {
        try {
            return getTokenClaims(token).getSubject();
        } catch (Exception e) {
            return null;
        }
    }

    /**
     * 获取Token载体信息
     *
     * @param token JWT
     * @return token携带的claim
     */
    private static Claims getTokenClaims(String token) {
        Claims claims = null;
        try {
            claims = Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody();
        } catch (ExpiredJwtException | UnsupportedJwtException | MalformedJwtException | SignatureException | IllegalArgumentException e) {
            e.printStackTrace();
        }
        return claims;
    }

    /**
     * 获取token携带的用户角色列表
     *
     * @param token JWT
     * @return 用户角色, 以英文逗号(, )分隔开
     */
    public static String getUserRole(String token) {
        return (String) getTokenClaims(token).get(ROLE);
    }

    /**
     * 获取token携带的用户ID
     *
     * @param token JWT
     * @return 用户ID
     */
    public static int getUserId(String token) {
        return getTokenClaims(token).get(USER_ID, Integer.class);
    }

    /**
     * 判断token是否过期
     *
     * @param token JWT
     * @return 是否过期
     */
    public static Boolean isExpiration(String token) {
        return getTokenClaims(token).getExpiration().before(new Date());
    }

    /**
     * 获取签发日期
     *
     * @param token JWT
     * @return 签发Token的日期
     */
    public static Date getIssuedAt(String token) {
        return getTokenClaims(token).getIssuedAt();
    }
}

接着是配置失败的统一处理,这个也很简单,默认是失败返回403,这里根据需求对其定制

ResultEntity以及StatusCode均是自定的类,输出string也是一样的

public class LoginFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException {
        Gson gson = new Gson();
        response.setCharacterEncoding("UTF-8");
        response.getWriter().print(gson.toJson(ResultEntity.error(StatusCode.USER_CREDENTIALS_ERROR)));
        response.setStatus(HttpStatus.OK.value());
    }
}

UserDetailService

UsernamePasswordAuthenticationFilter会调用UserDetailService的loadUsername方法,一般需要在系统中重写service

首先定义鉴权的类,这个系统中通过userId唯一区分用户,因此需要重写

如果通过用户名唯一区分,直接使用org.springframework.security.core.userdetails.User可以不用重写

public class BallUser extends org.springframework.security.core.userdetails.User {
    private final int userId;

    public BallUser(int userId, String username, String password, Collection<? extends GrantedAuthority> authorities) {
        super(username, password, authorities);
        this.userId = userId;
    }

    public BallUser(int userId, String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {
        super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
        this.userId = userId;
    }

    public int getUserId() {
        return userId;
    }
}

接着重写service

@Service
public class UserService implements UserDetailsService {
    @Autowired
    UserDao userDao;
    @Autowired
    RoleDao roleDao;
    @Autowired
    PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userDao.selectUserByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException("用户名不存在");
        }
        List<String> roles = roleDao.selectRolesByUserId(user.getUserId());
        roles.add(RoleEnum.COMMON.getRoleName());
        for (int i = 0; i < roles.size(); i++) {
            roles.set(i, "ROLE_" + roles.get(i));
        }
        String roleString = StringUtils.arrayToCommaDelimitedString(roles.toArray());
        return new BallUser(
                user.getUserId(),
                user.getUsername(),
                user.getPassword(),
                AuthorityUtils.commaSeparatedStringToAuthorityList(roleString));
    }

    public Boolean register(String username, String password) {
        if (userDao.selectUserByUsername(username) == null) {
            return userDao.insertUser(username, passwordEncoder.encode(password));
        }
        return false;
    }

    public List<String> loadRolesByUserId(int userId){
        List<String> roles = roleDao.selectRolesByUserId(userId);
        roles.add(RoleEnum.COMMON.getRoleName());
        for (int i = 0; i < roles.size(); i++) {
            roles.set(i, "ROLE_" + roles.get(i));
        }
        return roles;
    }
}

配置Token拦截

config

首先还是配置config

/**
 * Token登录的配置
 */
@Configuration
public class JwtLoginConfig<T extends AbstractHttpConfigurer<T, B>, B extends HttpSecurityBuilder<B>>
        extends AbstractHttpConfigurer<T, B> {
    @Override
    public void configure(B builder) throws Exception {
        builder.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
    }

    @Bean
    JwtAuthenticationFilter jwtAuthenticationFilter() {
        // 使用自定义的filter
        JwtAuthenticationFilter filter = new JwtAuthenticationFilter();
        // 新建providerManager并为filter配置provider
        ProviderManager providerManager = new ProviderManager(new JwtAuthenticationProvider());
        filter.setAuthenticationManager(providerManager);
        // 不使用session
        filter.setSessionAuthenticationStrategy(new NullAuthenticatedSessionStrategy());
        // 通过bean注入handler
        filter.setAuthenticationSuccessHandler(jwtLoginSuccessHandler());
        filter.setAuthenticationFailureHandler(new LoginFailureHandler());
        return filter;
    }

    @Bean
    JwtLoginSuccessHandler jwtLoginSuccessHandler(){
        return new JwtLoginSuccessHandler();
    }
}

Filter

接着自定义filter,这里的jwt拦截需要我们自定义拦截方式

public class JwtAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

    public JwtAuthenticationFilter() {
        // 拦截请求头中带有Authentication的
        super(new RequestHeaderRequestMatcher(JwtUtils.TOKEN_HEADER));
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
        response.setCharacterEncoding("UTF-8");
        response.setHeader("Content-Type", "text/html;charset=UTF-8");
        String token = getJwtToken(request);
        JwtAuthenticationToken authRequest = new JwtAuthenticationToken(null, token, null);
        return getAuthenticationManager().authenticate(authRequest);
    }

    // 这里一定要注意,必须重写,否则controller收不到请求
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        chain.doFilter(request, response);
    }

    private String getJwtToken(HttpServletRequest request) {
        String header = request.getHeader(JwtUtils.TOKEN_HEADER);
        return JwtUtils.tokenFromHeader(header);
    }
}

Token(鉴权信息容器)

自定义Token用以保存鉴权的信息

public class JwtAuthenticationToken extends AbstractAuthenticationToken {
    // 保持与UsernameAuthentication的一致性
    private final BallUser principal;
    private final Object credentials;

    public JwtAuthenticationToken(BallUser user, String token, Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = user;
        this.credentials = token;
    }

    @Override
    public BallUser getPrincipal() {
        return principal;
    }

    @Override
    public Object getCredentials() {
        return credentials;
    }
}

Provider

接着自定义验证的Provider

// @Component 这个component也是不需要的
public class JwtAuthenticationProvider implements AuthenticationProvider {
    // 这里注意,由于前面的实例都是new出来的,没有走spring的bean管理,这儿是没办法直接注入的
    // 需要获取bean
    // @Autowired
    // RoleDao roleDao;

      @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        JwtAuthenticationToken jwtToken = (JwtAuthenticationToken) authentication;
        String token = (String) jwtToken.getCredentials();
        try {
            int userId = JwtUtils.getUserId(token);
            // 获取service bean
            UserService userService = GetBeanUtils.getBean(UserService.class);
            List<String> roles = userService.loadRolesByUserId(userId);
            BallUser user = new BallUser(
                    userId, "[NotNeed]", "[Protected]",
                    AuthorityUtils.commaSeparatedStringToAuthorityList(StringUtils.arrayToCommaDelimitedString(roles.toArray())));
            return new JwtAuthenticationToken(user, token, user.getAuthorities());
        } catch (JwtException e) {
            e.printStackTrace();
            throw new AuthenticationServiceException("Token invalidate");
        }
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return authentication.isAssignableFrom(JwtAuthenticationToken.class);
    }
}

Spring Security通过鉴权过程中的异常来判断是否鉴权成功的,如果中途任何地方抛出AuthenticationException异常,即认为鉴权失败,没有任何异常返回结果认为鉴权成功

附录:

GetBeanUtils的内容参见 这篇博客

GetBeanUtils内容如下

// [这篇博客](https://blog.csdn.net/qq_28080659/article/details/99687074)
@Component
public class GetBeanUtils implements ApplicationContextAware {
    private static ApplicationContext applicationContext = null;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        if (GetBeanUtils.applicationContext == null) {
            GetBeanUtils.applicationContext = applicationContext;
        }
    }

    /**
     * 返回ApplicationContext
     *
     * @return ApplicationContext
     */
    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }

    /**
     * 返回bean
     *
     * @param beanName beanName
     * @return bean
     */
    public static Object getBean(String beanName) {
        return applicationContext.getBean(beanName);
    }

    /**
     * 获取bean
     *
     * @param c c
     * @param <T> 泛型
     * @return bean
     */
    public static <T> T getBean(Class<T> c) {
        return applicationContext.getBean(c);
    }

    /**
     * 获取bean
     * @param c c
     * @param  name 名称
     * @param <T> 泛型
     * @return T 泛型
     */
    public static <T> T getBean(String name, Class<T> c) {
        return getApplicationContext().getBean(name, c);
    }
}

三. Controller层验证

controller代码如下

@RestController
@RequestMapping("/")
public class UserController {
    @Autowired
    UserService userService;

    @RequestMapping("register")
    ResultEntity register(@RequestParam String username, @RequestParam String password) {
        if (userService.register(username, password)) {
            return ResultEntity.success();
        } else {
            return ResultEntity.error(StatusCode.USER_ALREADY_EXISTING);
        }
    }

    @RequestMapping("logout")
    @PreAuthorize("hasRole('COMMON')")
    // @PreAuthorize("hasRole('ROLE_COMMON')") 这么写与上一行等价,ROLE_可省
    ResultEntity logout() {
        return ResultEntity.success();
    }
}
  1. 不需要权限的接口 /register直接访问

    image-20211020221125050

  2. 需要权限的接口 /logout需要权限

    在有权限的情况下

    image-20211020221248748

    略微修改上面的controller代码,再次请求接口

    @RequestMapping("logout")
    @PreAuthorize("hasRole('SYSTEM')")
    ResultEntity logout() {
        return ResultEntity.success();
    }
    

    image-20211020221546136

    这里会显示没有权限返回403

  3. 隐含的拦截器接口 /login

    image-20211020221150172

一些注意事项

  1. 用到autowired的类,除了必须要有@Component/@Service等注解意外,必须要保证,其声明周期由springboot管理,而不是直接new出来的
  2. jwt要求的规范一定要注意,header是Authoritarian内容是bearer紧跟token,特别注意bearer后面有个space空格
  3. 重写AbstractAuthenticationProcessingFilter的时候,必须重写successfulAuthentication方法,否则后面的请求链会收不到请求,重写的时候需要添加chain.doFilter(request, response);
  4. 如果在new出来的类中想要使用springbean,可以参照配置token拦截的最后一部分使用GetBeanUtil
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值