Spring Security 从入门到实战

MicroBoy 2020-07-05

一、前言

Spring Security 和 Apache Shiro 都是安全框架,为Java应用程序提供身份认证和授权。本片文章将讲述Security入门到项目实战使用。其中包括简单的登录和项目常用的权限管理,权限校验有两种方式、角色资源校验、URL校验,如果有一定基础的可以想找解决办法的可以直接跳过前面基础直接寻找自己需要的解答内容。

二者区别

  1. Spring Security:重量级安全框架
  2. Apache Shiro:轻量级安全框架
    关于shiro的权限认证与授权,(这个以后有需要再出一篇关于 Shiro 的内容)

二、SpringBoot集成Spring Security入门体验

基本环节:Spring Boot 2.1.8

1、引入Spring Security依赖

<!--添加Spring securit依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

## 2、新建一个controller 测试访问```java@RestControllerpublic class IndexController { @GetMapping("/index") public String index() { return "Hello World ~"; }}```

3、运行项目访问:http://127.0.0.1:8080/index

温馨小提示:在不进行任何配置的情况下,Spring Security 给出的默认用户名为user?密码则是项目在启动运行时随机生成的一串字符串,会打印在控制台,如下图:




当我们访问index首页的时候,系统会默认跳转到login页面进行登录认证




认证成功之后才会跳转到我们的index页面

三、Spring Security用户密码配置

除了上面Spring Security在不进行任何配置下默认给出的用户user 密码随项目启动生成随机字符串,我们还可以通过以下方式配置

1、springboot配置文件中配置

spring:
 security:
   user:
     name: admin  # 用户名
     password: 123456  # 密码

2、java代码在内存中配置

新建 Security 核心配置类继承 WebSecurityConfigurerAdapter

@Configuration
@EnableWebSecurity // 启用Spring Security的Web安全支持
public class SecurityConfig extends WebSecurityConfigurerAdapter {
   /**
    * 将用户设置在内存中
    * @param auth
    * @throws Exception
    */
   @Autowired
   public void config(AuthenticationManagerBuilder auth) throws Exception {
       // 在内存中配置用户,配置多个用户调用`and()`方法
       auth.inMemoryAuthentication()
           // 指定加密方式
           .passwordEncoder(passwordEncoder())
           // 设置访问用户名
           .withUser("admin")
           // 设置密码
           .password(passwordEncoder().encode("123456"))
           // 用户所拥有的角色权限(注:这里后续会用于权限校验)
           .roles("ADMIN")
           .and()
           // 如果想设置多个可以重复添加
           .withUser("test").password(passwordEncoder().encode("123456")).roles("USER");
   }
   @Bean
   public PasswordEncoder passwordEncoder() {
       // BCryptPasswordEncoder:Spring Security 提供的加密工具,可快速实现加密加盐
       return new BCryptPasswordEncoder();
   }
}

3、从数据库中获取用户账号、密码信息

往往实际使用中会比上述情况复杂很多。
(动态添加用户,绑定角色,角色绑定权限资源等),这个留到文章后面鉴权内容再说。

四、Spring Security 登录处理 与 忽略拦截

最常用的简单配置,相关代码都有注释相信很容易理解

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
   /**
    * 登录处理
    * @param http
    * @throws Exception
    */
   @Override
   protected void configure(HttpSecurity http) throws Exception {
       
       //==========================================路径相关配置start
       
       // antMatcher():配置需要处理的URL  authorizeRequests():为URL配置授权事项,例如是否需要进行身份验证或仅某些角色可以访问它等
        ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = http.antMatcher("/**").authorizeRequests();
       
       // 标记只能在 服务器本地ip[127.0.0.1或者localhost] 访问`/home`接口,其他ip地址无法访问
        registry.antMatchers("/home").hasIpAddress("127.0.0.1");

        // 标识访问 `/index` 这个接口,需要具备`ADMIN`角色
        registry.antMatchers("/index").hasRole("ADMIN");

        // 允许匿名的url - 可理解为方形接口 - 多个接口使用,分割
        registry.antMatchers("/login", "/index").permitAll();

        // OPTIONS(选项):查找适用于一个特定网址资源的通讯选择。 在不需执行具体的涉及数据传输的动作情况下, 允许客户端来确定与资源相关的选项以及 / 或者要求, 或是一个服务器的性能
        registry.antMatchers(HttpMethod.OPTIONS, "/**").denyAll();

        // 其余所有请求都需要认证
        registry.anyRequest().authenticated();
       
       //==========================================路径相关配置end
       
       
       // 禁用CSRF 开启跨域
        http.csrf().disable().cors();

        //配置HTTP基本身份认证
        http.httpBasic();
       
     
       
       // 设置登录认证页面
       http.formLogin().loginPage("/login")
           // 登录后的处理接口 - 方式①
           .loginProcessingUrl("/user/login")
           // 自定义登陆用户名和密码属性名,默认为 username和password
           .usernameParameter("username")
           .passwordParameter("password")
           // 登录成功后的处理器  - 方式② 可以通过处理器处理返回信息(注:这里可以单独提出来写一个类)
           //                .successHandler((req, resp, authentication) -> {
           //                    resp.setContentType("application/json;charset=utf-8");
           //                    PrintWriter out = resp.getWriter();
           //                    out.write("登录成功...");
           //                    out.flush();
           //                })
           // 配置登录失败的回调 同上可以提取
           .failureHandler((req, resp, exception) -> {
               resp.setContentType("application/json;charset=utf-8");
               PrintWriter out = resp.getWriter();
               out.write("登录失败...");
               out.flush();
           })
           .logout().logoutUrl("/logout")
           // 配置注销成功的回调
           .logoutSuccessHandler((req, resp, authentication) -> {
               resp.setContentType("application/json;charset=utf-8");
               PrintWriter out = resp.getWriter();
               out.write("注销成功...");
               out.flush();
           })
           // 和表单登录相关的接口都通过
           .permitAll()
   }
   /**
    * 忽略拦截
    * @param web
    * @throws Exception
    */
   @Override
   public void configure(WebSecurity web) throws Exception {
       // 设置拦截忽略url - 会直接过滤该url - 将不会经过Spring Security过滤器
       web.ignoring().antMatchers("/getUserInfo");
       // 设置拦截忽略文件夹,可以对静态资源放行
       web.ignoring().antMatchers("/css/**", "/js/**");
   }
}

五、Spring Security 基于动态角色资源权限校验

上面我们了解到了 Security 是怎么进行基础配置的,下面我们要进行对数据库访问和权限分配的操作。
这一节主要使用权限资源对请求权限进行管理,也是前后端分离最常用的权限校验方式。希望大家看完会有所收获。

1、数据库结构设计

要实现权限控制首先得把数据库设计好。

我这边是这么设计的有一些字段可能比较多。仅供参考。

2、Security 配置调整

我们之前做了一些基础的配置,现在在这个基础的配置上进行改造。一步步打造成我们需要的功能。

2.1、将登录成功、登录失败、退出登录的处理类进行提取

  1. 登录成功处理类 AdminAuthenticationSuccessHandler
@Component
public class AdminAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        // SecurityUser 是Spring Security里面UserDetailsd的实现类里面包含了用户信息
        SecurityUser securityUser = ((SecurityUser) authentication.getPrincipal());
        // 从SecurityUser 中取出Token
        String token = securityUser.getToken();
		// 定义一个map集合 用于返回JSON数据
        HashMap<String, String> map = new HashMap<String, String>(1);
        // 前端会收到 {"token":"token值"} 
        map.put("token",token);
        // 结果集封装 规范返回内容
        Object result = ApiResult.ok(map);
        // 将封装数据进行返回。 这里有一个返回工具类
        ResponseUtils.renderJson(httpServletResponse, JacksonUtil.toJson(result));
    }
}

2. **SecrityUser 类** `SecurityUser`?

上面提到了 SecurityUser 类 ,这里贴一下我的代码

// 注解 避免JSON返回不必要的空数据
@JsonIgnoreProperties(ignoreUnknown = true)
public class SecurityUser implements UserDetails {
    /**
     * 当前登录用户
     */
    private User currentUserInfo;

    /**
     * 角色
     */
    private List<Role> roleList;

    /**
     * 用户权限值
     */
    private List<Permission> permissionList;


    private String token;


    /**
     * 空参构造
     */
    public SecurityUser() {
    }

    /**
     * 用户信息构造
     *
     * @param user
     */
    public SecurityUser(User user) {
        if (user != null) {
            this.currentUserInfo = user;
        }
    }

    public SecurityUser(User user, List<Role> roleList) {
        if (user != null) {
            this.currentUserInfo = user;
            this.roleList = roleList;
        }
    }

    public SecurityUser(User user, List<Role> roleList, List<Permission> permissionList) {
        if (user != null) {
            this.currentUserInfo = user;
            this.roleList = roleList;
            this.permissionList = permissionList;
        }
    }


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

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

    /**
     * 用户是否过期
     *
     * @return
     */
    @Override
    public boolean isAccountNonExpired() {
        return false;
    }

    /**
     * 用户是否被锁定
     *
     * @return true 用户被锁定  false用户没有被锁定
     */
    @Override
    public boolean isAccountNonLocked() {
        return false;
    }

    /**
     * 账户登录凭证是否过期
     *
     * @return
     */
    @Override
    public boolean isCredentialsNonExpired() {
        return false;
    }

    /**
     * 用户是否被禁用
     *
     * @return true是  false否
     */
    @Override
    public boolean isEnabled() {
        return false;
    }


    public String getToken() {
        return token;
    }

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

    public User getCurrentUserInfo() {
        return currentUserInfo;
    }

    public List<Role> getRoleList() {
        return roleList;
    }

    public List<Permission> getPermissionList() {
        return permissionList;
    }


    /**
     * 获取当前用户具有的角色
     * 用户添加权限。返回权限对象
     * 这个方法很重要,会影响到后面的权限资源控制
     * @return GrantedAuthority
     */
    @JsonIgnore
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> authorities = new HashSet<>();
        // 判断权限列表是否为空
        if (!CollectionUtils.isEmpty(this.permissionList)) {
            // 对权限列表进行遍历
            for (Permission permission : this.permissionList) {
                SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permission.getAccessCode());
                // 将权限添加到集合中
                authorities.add(authority);
            }
        }
        //返回权限
        return authorities;
    }
}

  1. 登录失败处理方法 AdminAuthenticationFailureHandler
@Slf4j
@Component
public class AdminAuthenticationFailureHandler implements AuthenticationFailureHandler {

    @Override
    public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
        Object result;
        // 对异常信息进行封装处理返回给前端
        if (e instanceof UsernameNotFoundException || e instanceof BadCredentialsException) {
            result = ApiResult.fail(HttpStatus.PAYMENT_REQUIRED.code(),e.getMessage());
        } else if (e instanceof LockedException) {
            result = ApiResult.fail(HttpStatus.PAYMENT_REQUIRED.code(),"账户被锁定,请联系管理员!");
        } else if (e instanceof CredentialsExpiredException) {
            result = ApiResult.fail(HttpStatus.PAYMENT_REQUIRED.code(),"证书过期,请联系管理员!");
        } else if (e instanceof AccountExpiredException) {
            result = ApiResult.fail(HttpStatus.PAYMENT_REQUIRED.code(),"账户过期,请联系管理员!");
        } else if (e instanceof DisabledException) {
            result = ApiResult.fail(HttpStatus.PAYMENT_REQUIRED.code(),"账户被禁用,请联系管理员!");
        } else {
            log.error("登录失败:", e);
            result = ApiResult.fail(HttpStatus.PAYMENT_REQUIRED.code(),"登录失败!");
        }
        ResponseUtils.renderJson(response, JacksonUtil.toJson(result));
    }
}

4. **退出登录处理** `AdminLogoutSuccessHandler`?```java@Componentpublic class AdminLogoutSuccessHandler implements LogoutSuccessHandler { @Override public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException { Object result = ApiResult.ok("退出成功"); ResponseUtils.renderJson(httpServletResponse, result); }}```

2.2、Security 配置类

SecurityConfig

@EnableWebSecurity
// 开启注解权限资源控制
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 自定义认证处理器 (重点:额外添加的类,用于获取用户信息,保存用户信息。并且处理登录等操作)
     */
    private final AdminAuthenticationProvider adminAuthenticationProvider;

    /**
     * 登录成功处理器
     */
    private final AdminAuthenticationSuccessHandler adminAuthenticationSuccessHandler;

    /**
     * 登录失败处理器
     */
    private final AdminAuthenticationFailureHandler adminAuthenticationFailureHandler;

    /**
     * 未登录情况下的处理类 (额外添加的类 这个类在下方有说明)
     */
    private final AdminAuthenticationEntryPoint adminAuthenticationEntryPoint;

    /**
     * Token 处理器 (额外添加的类 这个类在下方有说明)
     */
    private final MyTokenAuthenticationFilter myTokenAuthenticationFilter;

    /**
     * 退出登录处理类 (我在这里处理了一下缓存的token退出的时候清除掉了,这个类下方也有说明)
     */
    private final AdminLogoutHandler adminLogoutHandler;

    /**
     * 退出成功处理
     */
    private final AdminLogoutSuccessHandler adminLogoutSuccessHandler;

    // 上面是登录认证相关  下面为url权限相关 - ========================================================================================

    /**
     * 登陆过后无权访问返回处理类 (返回无权的时候的返回信息处理)
     */
    private final MyAccessDeniedHandler myAccessDeniedHandler;

    public SecurityConfig(AdminAuthenticationProvider adminAuthenticationProvider, AdminAuthenticationSuccessHandler adminAuthenticationSuccessHandler, AdminAuthenticationFailureHandler adminAuthenticationFailureHandler, AdminAuthenticationEntryPoint adminAuthenticationEntryPoint, MyTokenAuthenticationFilter myTokenAuthenticationFilter, AdminLogoutHandler adminLogoutHandler, AdminLogoutSuccessHandler adminLogoutSuccessHandler, MyAccessDeniedHandler myAccessDeniedHandler) {
        this.myTokenAuthenticationFilter = myTokenAuthenticationFilter;
        this.adminAuthenticationProvider = adminAuthenticationProvider;
        this.adminAuthenticationSuccessHandler = adminAuthenticationSuccessHandler;
        this.adminAuthenticationFailureHandler = adminAuthenticationFailureHandler;
        this.adminAuthenticationEntryPoint = adminAuthenticationEntryPoint;
        this.adminLogoutHandler = adminLogoutHandler;
        this.adminLogoutSuccessHandler = adminLogoutSuccessHandler;
        this.myAccessDeniedHandler = myAccessDeniedHandler;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = http.antMatcher("/**").authorizeRequests();

        // 标记只能在 服务器本地ip[127.0.0.1或者localhost] 访问`/home`接口,其他ip地址无法访问
        // registry.antMatchers("/").hasIpAddress("127.0.0.1");

        // 允许匿名的url - 可理解为放行接口 - 多个接口使用,分割
        registry.antMatchers("/home").permitAll();

        // OPTIONS(选项):查找适用于一个特定网址资源的通讯选择。 在不需执行具体的涉及数据传输的动作情况下, 允许客户端来确定与资源相关的选项以及 / 或者要求, 或是一个服务器的性能
        // registry.antMatchers(HttpMethod.OPTIONS, Constants.CONTEXT_PATH+"/**").denyAll();

        // 其余所有请求都需要认证
        registry.anyRequest().authenticated();

        // 禁用CSRF 开启跨域
        http.csrf().disable();

        // 开启CSRF 向前端发送 XSRF-TOKEN Cookie(上面设置了关闭,可以通过下述方式进行开启,但是swagger-ui的跨域403异常暂时找不到解决办法。无奈放弃了)
        // http.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());

        //配置HTTP基本身份认证
        http.httpBasic();

        // 未登录认证异常
        http.exceptionHandling().authenticationEntryPoint(adminAuthenticationEntryPoint);

        // 登陆过后无权访问返回
        http.exceptionHandling().accessDeniedHandler(myAccessDeniedHandler);

        // 登录处理 - 前后端一体的情况下
        http.formLogin().loginProcessingUrl("/user/login")
                    // 默认的登录成功返回url
                    // .defaultSuccessUrl("/")
                    // 登录成功处理
                    .successHandler(adminAuthenticationSuccessHandler)
                    .failureHandler(adminAuthenticationFailureHandler)
                    // 自定义登陆用户名和密码属性名,默认为 username和password
                    .usernameParameter("username").passwordParameter("password")
                    // 异常处理
                    // .failureUrl(Constants.CONTEXT_PATH+"/login/error").permitAll()
                    // 退出登录
                    .and().logout().logoutUrl("/user/logout").permitAll()
                    .addLogoutHandler(adminLogoutHandler)
                    .logoutSuccessHandler(adminLogoutSuccessHandler);

        // session创建规则  STATELESS 不使用session
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

        // 防止iframe 造成跨域
        http.headers().frameOptions().disable();

        // 添加前置的过滤器 用于验证token
        http.addFilterBefore(myTokenAuthenticationFilter, BasicAuthenticationFilter.class);
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 自定义验证管理器
        auth.authenticationProvider(adminAuthenticationProvider);
        // super.configure(auth);
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        // ignoring 允许添加 RequestMatcher Spring Security 应该忽略的实例。
        web.ignoring().antMatchers(HttpMethod.GET,
                "/favicon.ico",
                "/*.html",
                "/**/*.css",
                "/**/*.js");
        web.ignoring().antMatchers(HttpMethod.GET,"/swagger-resources/**");
        web.ignoring().antMatchers(HttpMethod.GET,"/webjars/**");
        web.ignoring().antMatchers(HttpMethod.GET,"/v2/api-docs");
        web.ignoring().antMatchers(HttpMethod.GET,"/v2/api-docs-ext");
        web.ignoring().antMatchers(HttpMethod.GET,"/configuration/ui");
        web.ignoring().antMatchers(HttpMethod.GET,"/configuration/security");
    }
}

2.3、自定义认证处理器

用于从获取用户信息、保存用户信息,并且处理登录等操作。
AdminAuthenticationProvider

@Component
public class AdminAuthenticationProvider implements AuthenticationProvider {

    private final
    UserDetailsServiceImpl userDetailsService;

    private final RedisUtil redisUtil;

    @Autowired
    public AdminAuthenticationProvider(UserDetailsServiceImpl userDetailsService, RedisUtil redisUtil) {
        this.userDetailsService = userDetailsService;
        this.redisUtil = redisUtil;
    }
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        // 获取前端表单中输入后返回的用户名、密码
        String userName = (String) authentication.getPrincipal();
        String password = (String) authentication.getCredentials();

        // 通过用户名称获取用户信息 (UserDetailsServiceImpl 这个类是实现了 Security 中提供的 UserDetailsService 用于处理用户信息 具体内容在下方)
        SecurityUser userInfo = (SecurityUser) userDetailsService.loadUserByUsername(userName);

        // 判断用户是否存在
        if(userInfo == null){
            throw new UsernameNotFoundException("当前用户名不存在");
        }
        // 对可能出现的异常进行抛出,抛出的一次会跳到登录失败中进行集中处理
        if(userInfo.isEnabled()){
            throw new DisabledException("该账户已被禁用,请联系管理员");
        }else if(userInfo.isAccountNonLocked()){
            throw new LockedException("该账户已被锁定");
        }else if(userInfo.isAccountNonExpired()){
            throw new AccountExpiredException("该账户已过期,请联系管理员");
        }else if(userInfo.isCredentialsNonExpired()){
            throw new CredentialsExpiredException("该账户的登陆凭证已过期,请重新登录");
        }

        // 密码验证,用于验证传进来的密码和加密后的密码是否是同一个。
        boolean isValid = PasswordUtils.checkpw(password, userInfo.getPassword());
        if (!isValid) {
            //抛出密码异常
            throw new BadCredentialsException("密码错误!");
        }

        // 前后端分离情况下 处理逻辑...
        // 更新登录令牌 - 之后访问系统其它接口直接通过token认证用户权限...
        String token = PasswordUtils.hashpw(System.currentTimeMillis() + userInfo.getUsername(), PasswordUtils.getSalt());
        userInfo.setToken(token);
        // 通过 redis 缓存数据
        redisUtil.set(token,userInfo,30*60L);

        return new UsernamePasswordAuthenticationToken(userInfo,password,userInfo.getAuthorities());
    }

    @Override
    public boolean supports(Class<?> aClass) {
        //确保 aClass 能转成该类
        return aClass.equals(UsernamePasswordAuthenticationToken.class);
    }
}

UserDetailsServiceImpl?
上面有提到所以贴出来方便理解整个过程,这个类用于处理用户数据,获取用户信息。

@Service("userDetailsService")
public class UserDetailsServiceImpl implements UserDetailsService {

    private final UserService userService;

    private final UserRoleService userRoleService;

    private final RoleService roleService;

    private final RolePermissionService rolePermissionService;

    private final PermissionService permissionService;

    @Autowired(required = false)
    public UserDetailsServiceImpl(UserMapper userMapper, RoleMapper roleMapper, PermissionMapper permissionMapper, UserRoleMapper userRoleMapper, RolePermissionMapper rolePermissionMapper, GroupPermissionMapper groupPermissionMapper, UserService userService, UserRoleService userRoleService, RoleService roleService, RolePermissionService rolePermissionService, PermissionService permissionService) {
        this.userService = userService;
        this.userRoleService = userRoleService;
        this.roleService = roleService;
        this.rolePermissionService = rolePermissionService;
        this.permissionService = permissionService;
    }

    /**
     * 根据用户获取用户信息、角色信息、权限信息
     *
     * @param username:
     * @return UserDetails
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 从数据库中取出用户信息
        User user = userService.queryByUserName(username);
        // 判断用户是否存在
        if (user == null) {
            throw new UsernameNotFoundException("用户名不存在");
        }
        List<Role> userRoles = getUserRoles(user.getId());
        List<Permission> userRolePermissions = getUserRolePermissions(userRoles);
        // 返回 SecurityUser 这个类上面有介绍
        return new SecurityUser(user,userRoles,userRolePermissions);
    }

    /**
     * 根据用户ID获取角色权限信息
     *
     * @param id
     * @return
     */
    private List<Role> getUserRoles(Integer id) {
        //通过用户ID查询角色中间表
        List<UserRole> userRoles = userRoleService.queryAll(id);
        //通过角色中间表查询角色列表
        List<Role> roles = roleService.queryByUserRoles(userRoles);
        // 返回角色列表
        return roles;
    }

    /**
     * 通过角色列表获取权限信息字符串
     *
     * @return
     */
    private Set<String> getUserRolePermissionsAccessCode(List<Role> roleList) {
        /**
         * 通过角色权限中间表查询
         */
        List<RolePermission> rolePermissions = rolePermissionService.queryByRoleList(roleList);

        return permissionService.queryByPermissionIdsOrAccessCode(rolePermissions);
    }

    /**
     * 通过角色列表获取权限信息
     *
     * @return
     */
    private List<Permission> getUserRolePermissions(List<Role> roleList) {
        /**
         * 通过角色权限中间表查询
         */
        List<RolePermission> rolePermissions = rolePermissionService.queryByRoleList(roleList);

        return permissionService.queryByPermissionIds(rolePermissions);
    }
}

### 2.4、用户 Token 处理`MyTokenAuthenticationFilter`?```java@Slf4j@Componentpublic class MyTokenAuthenticationFilter extends OncePerRequestFilter {
private final RedisUtil redisUtil;

public MyTokenAuthenticationFilter(RedisUtil redisUtil) {
    this.redisUtil = redisUtil;
}

@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
    log.info("请求头类型: {}" , httpServletRequest.getContentType());

    MultiReadHttpServletRequest wrappedRequest = new MultiReadHttpServletRequest(httpServletRequest);
    MultiReadHttpServletResponse wrappedResponse = new MultiReadHttpServletResponse(httpServletResponse);
    StopWatch stopWatch = new StopWatch();
    try {
        stopWatch.start();
        // 记录请求的消息体
        logRequestBody(wrappedRequest);

        // 前后端分离情况下,前端登录后将token储存在cookie中,每次访问接口时通过token去拿用户权限
        // Constants 这个类是自己定义的常量类REQUEST_HEADER这个表示token头的名称
        String token = wrappedRequest.getHeader(Constants.REQUEST_HEADER);
        log.debug("后台检查令牌:{}", token);

        if (StringUtils.isNotBlank(token)) {
            // 检查token
            SecurityUser securityUser = (SecurityUser) redisUtil.get(token);
            if (securityUser == null || securityUser.getCurrentUserInfo() == null) {
                throw new AccessDeniedException("TOKEN已过期,请重新登录!");
            }
            UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(securityUser, null, securityUser.getAuthorities());

            // 全局注入角色权限信息和登录用户基本信息
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        filterChain.doFilter(wrappedRequest, wrappedResponse);
    } finally {
        stopWatch.stop();
        long usedTimes = stopWatch.getTotalTimeMillis();
        // 记录响应的消息体
        logResponseBody(wrappedRequest, wrappedResponse, usedTimes);
    }
}

// 处理请求
private void logRequestBody(MultiReadHttpServletRequest request) {
    MultiReadHttpServletRequest wrapper = request;
    if (wrapper != null) {
        try {
            String bodyJson = wrapper.getBodyJsonStrByJson(request);
            String url = wrapper.getRequestURI().replace("//", "/");
            log.info("-------------------------------- 请求url: " + url + " --------------------------------");
            Constants.URL_MAPPING_MAP.put(url, url);
            log.info("`{}` 接收到的参数: {}",url , bodyJson);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
// 处理返回
private void logResponseBody(MultiReadHttpServletRequest request, MultiReadHttpServletResponse response, long useTime) {
    MultiReadHttpServletResponse wrapper = response;
    if (wrapper != null) {
        byte[] buf = wrapper.getBody();
        if (buf.length > 0) {
            String payload;
            try {
                payload = new String(buf, 0, buf.length, wrapper.getCharacterEncoding());
            } catch (UnsupportedEncodingException ex) {
                payload = "[unknown]";
            }
            log.info("`{}`  耗时:{}ms  返回的参数: {}", Constants.URL_MAPPING_MAP.get(request.getRequestURI()), useTime, payload);
            log.info("");
        }
    }
}

}

到这里其实已经基本上差不多了。<br />

### 2.5、其他处理类


1. **未登录情况下的处理类** `AdminAuthenticationEntryPoint.java`?
```java
@Slf4j
@Component
public class AdminAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        log.error(e.getMessage());
        // 其中HttpStatus类是自己定义的一个枚举类。
        ResponseUtils.renderJson(httpServletResponse, ApiResult.fail(HttpStatus.UNAUTHORIZED.code(),HttpStatus.UNAUTHORIZED.reasonPhraseCN()));
    }
}
  1. 登陆过后无权访问返回处理类 MyAccessDeniedHandler.java
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
        ResponseUtils.renderJson(httpServletResponse, ApiResult.fail(403, e.getMessage()));
    }
}


到这里配置项已经完成了。

3、在 Controller 中添加访问权限

这里我简单写了一下 UserController.java

@RestController
@RequestMapping("/api/user")
@Api(value = "用户相关操作",tags="用户相关操作")
public class UserController {

    private final
    UserMapper userMapper;

    @Autowired(required = false)
    public UserController(UserMapper userMapper) {
        this.userMapper = userMapper;
    }



    @PreAuthorize("hasAuthority(‘user‘)")
    @PostMapping("/userInfo")
    @ApiOperation(value = "查询用户信息",notes = "查询用户信息请求头里面需要携带X-Token")
    UserInfo getUserInfo() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        UserInfo userInfo = null;
        if(authentication != null){
            SecurityUser securityUser = (SecurityUser) authentication.getPrincipal();
            userInfo = new UserInfo();
            userInfo.setUser(securityUser.getCurrentUserInfo());
            userInfo.setRoles(securityUser.getRoleList());
            userInfo.setPermissions(securityUser.getPermissionList());
            userInfo.setPermissionAccessList(securityUser.getPermissionAccessCode());
        }
        return userInfo;
    }


    @PreAuthorize("hasAuthority(‘user_add‘)")
    @PostMapping("/addUser")
    @ApiOperation(value = "添加用户",notes = "添加用户信息")
    @ApiImplicitParam()
    Object addUser(@RequestBody User user){
        user.setPassword(PasswordUtils.hashpw(user.getPassword(),PasswordUtils.getSalt()));
        userMapper.insertSelective(user);
        return ApiResult.ok("操作成功");
    }

    @PreAuthorize("hasAuthority(‘no_access‘)")
    @PostMapping("/noAccess")
    @ApiImplicitParam()
    Object noAccess(){
        return ApiResult.ok("无权访问的接口");
    }
}

4、对上述的功能进行测试

写了这么多,也要对实际结果进行确认才行。我这里用的 Postman 测试一下。

  1. 首先需要登录获取 token


通过登录接口可以拿到token,token值需要放到请求头中。用于认证用户是否登录。
**

  1. 再测试一下其他接口

**

  • 获取用户信息(注意这里设置了 token 请求头)
  • 访问无权接口




这样的话就差不多是最终的效果了。多尝试多进步,方法总比困难多。
如果有更好的处理方式可以提出来相互交流~



这里还有一节通过URL控制权限。偷个懒以后再补充~~






未完待续


参考链接:
https://blog.csdn.net/qq_38225558/article/details/101754743

相关推荐

itjavashuai / 0评论 2020-07-27