javaWeb学习

Spring Boot 2 接口权限控制Spring Secur

2019-06-13  本文已影响40人  AC编程

一、任务描述

有一个测试接口AuthTestController

@RestController
@RequestMapping("/auth")
public class AuthTestController {

    @GetMapping()
    public String greeting() {
        return "Hello,Auth!";
    }
}

现在用Postman测试一下,如图:


AuthTestController测试结果

任何人只要知道了这个接口的地址,那么就可以在任何时候无障碍地访问这个接口并得到期望的返回值,这显然是不安全的,所以需要给这个接口加上权限控制,访问该接口的用户必须先授权,授权后再通过授权号来访问该接口。具体实现如下

二、添加Spring Security依赖

compile group: 'org.springframework.boot', name: 'spring-boot-starter-security', version: '2.0.5.RELEASE'

添加了Spring Security依赖后,我们再来访问该接口发现提示401,说明接口已经被保护起来了,需要授权才能正常访问


访问接口提示401

接下来我们就来添加提供授权的代码

三、编写提供授权的代码

3.1 继承WebSecurityConfigurerAdapter
@Configuration
@Order(SecurityProperties.BASIC_AUTH_ORDER)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    private final String DEV_ENVIRONMENT = "dev";

    /**
     * 运行环境:dev/prod/test
     */
    @Value("${spring.profiles.active}")
    private String active;

    /**
     * 密码加密及校验方式
     *
     * @return
     */
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * Web资源权限控制
     *
     * @param web
     * @throws Exception
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
        super.configure(web);
        web.ignoring().antMatchers("/config/**", "/css/**", "/fonts/**", "/img/**", "/js/**");

        //Ant Design登录页面,限定GET,避免和 Spring Security 的login(POST方式)冲突
        web.ignoring().antMatchers(HttpMethod.GET,"/login");

        //Ant Design 页面
        web.ignoring().antMatchers("/","/console", "/console/**","/static/**","/*.png","/*.js","/*.css");

        //swagger-ui start
        web.ignoring().antMatchers("/v2/api-docs/**");
        web.ignoring().antMatchers("/swagger.json");
        web.ignoring().antMatchers("/swagger-ui.html");
        web.ignoring().antMatchers("/swagger-resources/**");
        web.ignoring().antMatchers("/webjars/**");
        //swagger-ui end
    }

    /**
     * HTTP请求权限控制
     *
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //本地开发环境关闭权限控制,方便测试
        if(DEV_ENVIRONMENT.equals(active)){
            http.cors().and().csrf().disable().authorizeRequests()
                    .antMatchers("/**").permitAll()
                    .anyRequest().authenticated()
                    .and()
                    .addFilter(new JwtLoginFilter(authenticationManager()))
                    .addFilter(new JwtAuthenticationFilter(authenticationManager()));
        }else{
           http.cors().and().csrf().disable().authorizeRequests()
                    .antMatchers("/user-login/verify-account").permitAll()
                    .anyRequest().authenticated()
                    .and()
                    .addFilter(new JwtLoginFilter(authenticationManager()))
                    .addFilter(new JwtAuthenticationFilter(authenticationManager()));
        }

        // 禁用 SESSION、JSESSIONID
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }

}

注意:

.antMatchers("/user-login/verify-account").permitAll() 要写在.anyRequest().authenticated()前面,不然接口权限放行会无效
3.2 继承BasicAuthenticationFilter
public class JwtAuthenticationFilter extends BasicAuthenticationFilter {

    public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

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

        if (header == null || !header.startsWith(JwtUtils.getAuthorizationHeaderPrefix())) {
            chain.doFilter(request, response);
            return;
        }

        UsernamePasswordAuthenticationToken authenticationToken = getUsernamePasswordAuthenticationToken(header);

        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        chain.doFilter(request, response);
    }

    private UsernamePasswordAuthenticationToken getUsernamePasswordAuthenticationToken(String token) {
        String user = Jwts.parser()
                .setSigningKey("PrivateSecret") //私钥
                .parseClaimsJws(token.replace(JwtUtils.getAuthorizationHeaderPrefix(), ""))
                .getBody()
                .getSubject();

        if (null != user) {
            return new UsernamePasswordAuthenticationToken(user, null, new ArrayList<>());
        }

        return null;
    }
}
3.3 继承UsernamePasswordAuthenticationFilter
public class JwtLoginFilter extends UsernamePasswordAuthenticationFilter {

    private AuthenticationManager authenticationManager;

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

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException {

        String username = this.obtainUsername(request);
        String password = this.obtainPassword(request);

        return authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(
                        username,
                        password,
                        new ArrayList<>()
                )
        );
    }

    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response,
                                            FilterChain chain, Authentication authResult) {
        String token = Jwts.builder()
                .setSubject(((User) authResult.getPrincipal()).getUsername())
                .setExpiration(new Date(System.currentTimeMillis() + 30 * 60 * 1000))
                .signWith(SignatureAlgorithm.HS512, "PrivateSecret") //私钥
                .compact();

        returnToken(response, JwtUtils.getTokenHeader(token));
    }

    private void returnToken(HttpServletResponse response, String token) {

        JwtToken jwtToken = new JwtToken(token);
        JSONObject responseJSONObject = new JSONObject(jwtToken);

        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json; charset=utf-8");
        PrintWriter out = null;
        try {
            out = response.getWriter();
            out.append(responseJSONObject.toString());
            out.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (out != null) {
                out.close();
            }
        }
    }
}
3.4 实现接口UserDetailsService
@Service
public class UserDetailServiceImpl implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
        //TODO 从数据库取数据
        String password = "$2a$10$hoIKMK7haFkAShKNHctxceBSCigIFOkrjOh7XNDF8s0py14RNVkXW"; //admin BCryptPasswordEncoder加密后的字符串
        //String password = userServiceImp.getUserPassWord(userName);
        return new User(userName, password, getAuthority());  //emptyList()
    }

    private List getAuthority() {
        return Arrays.asList(new SimpleGrantedAuthority("ROLE_ADMIN"));
    }
}
3.5 JwtUtils
public class JwtUtil {
    private static final String AUTHORIZATION_HEADER_PREFIX = "Bearer ";

    public static String getTokenHeader(String rawToken) {
        return AUTHORIZATION_HEADER_PREFIX + rawToken;
    }

    public static String getAuthorizationHeaderPrefix() {
        return AUTHORIZATION_HEADER_PREFIX;
    }
}
3.6 JwtToken
public class JwtToken implements Serializable {

    private String token;

    public JwtToken(String token) {
        this.token = token;
    }

    public String getToken() {
        return token;
    }

    public void setToken(String token) {
        this.token = token;
    }
}

四、获得密码加密字符串

现假设账号密码都为admin,因为在上面的代码里面我们的密码加密及校验方式用的是BCryptPasswordEncoder,所以先手动获取一下admin加密后的字符串

public class Client {
    public static void main(String[] args) {
        BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
        String pwd = encoder.encode("admin");
        System.out.println(pwd);
    }
}

得到加密后的字符串

$2a$10$hoIKMK7haFkAShKNHctxceBSCigIFOkrjOh7XNDF8s0py14RNVkXW

将该加密后的字符串写死在UserDetailsService 实现类的loadUserByUsername方法里,正常应该是从数据通过用户名取出密码的,现为了简化测试,先手动写死。

五、通过账号、密码获取授权

通过POST方式,访问login接口得到授权号,注意要传参数用户名和密码


image.png

login方法是Spring Security内置的一个授权接口,查看源码如下


login接口源码

六、通过授权号访问测试接口

将刚才的授权号拷贝出来(不用拷贝前缀Bearer)

eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhZG1pbiIsImV4cCI6MTU2MDQxNzE1OH0.5wGV19nJR2HX6fB_2GdLlb6Q8khCA-6a9tyAOJXxpuuIProTCU3keLeFTrBQrowoOu_6dUs4Uz9uznC5eXy_sA

Authorization Type 选择 Bearer Token,并在Token输入框内输入刚才的授权号,发现能正常访问测试接口了,如下图:


授权访问

七、SecurityConfiguration具体介绍

现在我们重点来具体介绍一下SecurityConfiguration里相关代码的作用

7.1 Spring Security禁用session

我们先注释以下代码

//http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

我们首先传入授权号访问测试接口,测试结果是能正常访问,现在我们再来尝试一下不传人授权号来访问测试接口,看有什么反应,测试结果如下:


不传授权号仍然能访问测试接口

发现很神奇,不传授权号仍然能访问测试接口,到底是哪里出了问题?我们点开Postman窗口右上角的“Cookies”发现有JSESSIONID(session的一种),JSESSIONID是Spring Boot内嵌Tomcat生成的,就是这个JSESSIONID已经记录了我们上一次请求的信息,所以现在不传人授权号,仍然可以访问到测试接口

image.png

我们现在手动把JSESSIONID删除,再来测试看一下,发现已经提示403 访问失败,请求被Forbidden了,接口得到很好的保护,如下图:

不传授权号接口访问失败

我们现在用Postman测试可以手动删除JSESSIONID,那如果是其他的client(如web、android等)访问也会有这个问题,有没有什么办法让Spring Boot内嵌Tomcat不生产这个JSESSIONID?答案是:有办法。
刚才注释的那段代码就是用来禁用 SESSION、JSESSIONID的,我们把注释的代码打开再来测试,发现Cookies里就没有生成JSESSIONID了。

7.2 访问swagger

我们先注释以下代码

     // web.ignoring().antMatchers("/v2/api-docs/**");
     // web.ignoring().antMatchers("/swagger.json");
    // web.ignoring().antMatchers("/swagger-ui.html");
    // web.ignoring().antMatchers("/swagger-resources/**");
    // web.ignoring().antMatchers("/webjars/**");

如果接入了swagger作为接口文档,当添加Spring Security之后,发现之前能正常访问的接口文档现在访问不了了,原因是Spring Security对swagger的访问也被加上了权限控制,如下图:


swagger访问无权限

swagger一般只在本地开发或内部测试环境中使用,在生成环境会被关闭,所以我们期待swagger不要被权限控制(开发环境开启Swagger,生产环境关闭Swagger),刚才我们注释的那段代码就是用来配置swagger不要被权限控制,我们打开注释代码,重新启动服务器,访问swagger,如下图:

swagger访问正常
7.3 本地开发环境关闭权限校验

我们期望如果是本地开发环境则关闭权限校验,因为这样方便我们通过Postman测试接口,只有测试环境和生产环境时才开启全新校验。我们可以这样实现:

首先,我的配置文件有三套,分别对应本地开发环境、测试环境、生产环境,在application.yml里配置采用哪套配置文件,我们可以通过spring.profiles.active这个参数来取到当前的运行环境,然后设置是否开启权限校验,所以我们在SecurityConfiguration的方法configure(HttpSecurity http)里添加了一段代码,代码如下:


配置文件
//本地开发环境关闭权限控制,方便测试
        if("dev".equals(active)){
            http.cors().and().csrf().disable().authorizeRequests()
                    .antMatchers("/**").permitAll();
        }else{
            http.cors().and().csrf().disable().authorizeRequests()
                    .anyRequest().authenticated()
                    .and()
                    .addFilter(new JwtLoginFilter(authenticationManager()))
                    .addFilter(new JwtAuthenticationFilter(authenticationManager()));
        }
上一篇下一篇

猜你喜欢

热点阅读