spring全家桶系列之spring boot 2.2-spring security 基础登录配置(一)

简介

未更新此文档为spring boot2.2以下版本,因为内容较多,整理更新


Spring Security是一个功能强大且可高度自定义的身份验证和访问控制框架。它是保护基于Spring的应用程序的事实标准。

Spring Security是一个专注于为Java应用程序提供身份验证和授权的框架。与所有Spring项目一样,Spring Security的真正强大之处在于它可以轻松扩展以满足自定义要求

特征

  • 对身份验证和授权的全面和可扩展的支持

  • 防止会话固定,点击劫持,跨站点请求伪造等攻击

  • Servlet API集成

  • 可选与Spring MVC集成

主要介绍:基本登录 ,openId登录,oauth2登录


maven


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

  基于注解的认证


@EnableWebSecurity 开启security
@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true,jsr250Enabled = true)
 为基于方法的安全注解

1.securedEnabled = true 一般不使用

开启框架的原生@Secured注释,spring security底层注解

public interface BankService {

@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
public Account readAccount(Long id);

@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
public Account[] findAccounts();

@Secured("ROLE_TELLER")
public Account post(Account account, double amount);
}

 

2.prePostEnabled = true  使用最多

开启 @PreAuthorize 注解,先大体写个示例

    //表现层使用   
    @GetMapping("create")
    @PreAuthorize("hasRole('role_admin')")//hasRole为SpEL表达式
    public SysUser create(SysUser sysUser) {
        //...
    }
//接口中使用(表现层也可以使用)
public interface BankService {

@PreAuthorize("isAnonymous()")
public Account readAccount(Long id);

@PreAuthorize("isAnonymous()")
public Account[] findAccounts();

@PreAuthorize("hasAuthority('ROLE_TELLER')")
public Account post(Account account, double amount);
}

3.jsr250Enabled = true 一般不使用

由java提供JSR是Java Specification Requests的缩写,意思是Java 规范提案。是指向JCP(Java Community Process)提出新增一个标准化技术规范的正式请求。详情:链接


spring boot 配置security


自定义配置信息 

@Getter
@Setter
@ConfigurationProperties(prefix = "project.security")//配置前缀
public class SecurityProperties {
    // 免认证资源路径
    private String[] anonResourcesUrl;
    //登录地址
    private String loginUrl;
    //退出地址
    private String logoutUrl;
    //持久化登录是否在启动创建表
    private boolean createTableOnStartup;
    //超时时间默认30分钟
    private Duration sessionTimeout = Duration.ofMinutes(30);
    //最大并发登录数量,默认值为-1,表示无限制
    private int maximumSession;

}

  application.yml中添加

#自定义配置
project:
  security:
    #免认证资源路径
    anon-resources-url: /public/**,/static/**
    #登录地址
    login-url: /login
    #退出地址
    logout-url: /logout
    #30分钟
    session-timeout: PT30M
    #最大并发登录数量
    maximum-session: 1
    #持久化登录是否在启动创建表
    create-table-on-startup: false

 SecurityConfig 配置


 响应配置

public class AuthResponseIO extends HashMap<String, Object> {

    //200 登录成功
    public static AuthResponseIO ok(String msg) {
        AuthResponseIO AuthResponse = new AuthResponseIO();
        AuthResponse.put("code", HttpStatus.OK.value());
        AuthResponse.put("msg", msg);
        return AuthResponse;
    }

    //401 登录未成功
    public static AuthResponseIO unAuthorized(String msg) {
        AuthResponseIO AuthResponse = new AuthResponseIO();
        AuthResponse.put("code", HttpStatus.UNAUTHORIZED.value());
        AuthResponse.put("msg", msg);
        return AuthResponse;
    }

    //403
    public static AuthResponseIO forbidden() {
        AuthResponseIO AuthResponse = new AuthResponseIO();
        AuthResponse.put("code", HttpStatus.FORBIDDEN.value());
        AuthResponse.put("msg", "无权限访问!");
        return AuthResponse;
    }


    //404
    public static AuthResponseIO notFound() {
        AuthResponseIO AuthResponse = new AuthResponseIO();
        AuthResponse.put("code", HttpStatus.NOT_FOUND.value());
        AuthResponse.put("msg", "请求未找到!");
        return AuthResponse;
    }

    //500
    public static AuthResponseIO error() {
        AuthResponseIO AuthResponse = new AuthResponseIO();
        AuthResponse.put("code", HttpStatus.INTERNAL_SERVER_ERROR.value());
        AuthResponse.put("msg", "服务器内部错误!");
        return AuthResponse;
    }
    public static AuthResponseIO error(String msg) {
        AuthResponseIO AuthResponse = new AuthResponseIO();
        AuthResponse.put("code", HttpStatus.INTERNAL_SERVER_ERROR.value());
        AuthResponse.put("msg", msg);
        return AuthResponse;
    }


    @Override
    public AuthResponseIO put(String key, Object value) {
        super.put(key, value);
        return this;
    }
}

 

Security headler配置

/**
 * 登录成功处理器
 */
@Component
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    private ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        WebAuthenticationDetails details = (WebAuthenticationDetails) authentication.getDetails();
        //表示接收到身份验证请求的TCP / IP地址
        //RemoteAddress()无法伪造,因为建立TCP连接需要三次握手.如果伪造了源 IP,无法建立TCP连接, 
        //更不会有后面的HTTP请求,如果是代理地址使用下面的getIP。
        String remoteAddress = details.getRemoteAddress();
        Object principal = authentication.getPrincipal();
        if (principal instanceof MyUserDetails) {
            MyUserDetails userDetails = (MyUserDetails) principal;
            userDetails.setRemoteAddress(remoteAddress);
        }
        response.setContentType(ResponseContent.JSON_UTF8.getValue());
        response.setStatus(HttpStatus.OK.value());
        response.getWriter().write(objectMapper.writeValueAsString(AuthResponseIO.ok("登录成功!")));
    }
}

    /**
     * 获取IP地址
     * 使用 Ngnix等反向代理软件, 则不能通过request.getRemoteAddr()获取IP地址
     * 如果使用了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP地址,X-Forwarded-For中第一个非 unknown的有效IP字符串,则为真实IP地址
     */
    public static String getIP(HttpServletRequest request) {
        if (Objects.nonNull(request)) {
            String ip = request.getHeader("X-Forwarded-For");
            if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip))
                ip = request.getHeader("Proxy-Client-IP");
            if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip))
                ip = request.getHeader("WL-Proxy-Client-IP");
            if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip))
                ip = request.getHeader("HTTP_CLIENT_IP");
            if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip))
                ip = request.getHeader("HTTP_X_FORWARDED_FOR");
            if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip))
                ip = request.getRemoteAddr();
            if ("127.0.0.1".equals(ip) || "0:0:0:0:0:0:0:1".equals(ip))
                try {
                    ip = InetAddress.getLocalHost().getHostAddress();
                } catch (UnknownHostException e) {
                    log.error("未知主机异常{}", e.getMessage());
                }
            return ip;
        }
        return null;
    }
/**
 * 登录失败处理器
 */
@Component
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {

    private ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        String message;
        if (exception instanceof UsernameNotFoundException) {
            message = "用户不存在!";
        } else if (exception instanceof BadCredentialsException) {
            message = "密码错误!";
        } else if (exception instanceof LockedException) {
            message = "用户已被锁定!";
        } else if (exception instanceof DisabledException) {
            message = "用户不可用!";
        } else if (exception instanceof AccountExpiredException) {
            message = "账户已过期!";
        } else if (exception instanceof CredentialsExpiredException) {
            message = "用户密码已过期!";
        } else {
            message = "认证失败,请联系网站管理员!";
        }
        response.setContentType(ResponseContent.JSON_UTF8.getValue());
        response.sendError(HttpStatus.UNAUTHORIZED.value(), message);
        response.getWriter().write(objectMapper.writeValueAsString(AuthResponseIO.unAuthorized(message)));
    }
}

 

/**
 * 访问被拒绝处理
 */
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {

    private ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        response.setContentType(ResponseContent.JSON_UTF8.getValue());
        response.getWriter().write(objectMapper.writeValueAsString(AuthResponseIO.forbidden()));
        response.sendError(HttpStatus.FORBIDDEN.value(),"无权限访问!");
    }

    //不分离配置
//    private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
//
//    @Override
//    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
//        response.setContentType("application/json;charset=utf-8");
//        response.getWriter().write(objectMapper.writeValueAsString(AuthResponseIO.forbidden()));
//        response.sendError(HttpStatus.FORBIDDEN.value(), HttpStatus.FORBIDDEN.getReasonPhrase());
//        redirectStrategy.sendRedirect(request, response, "/403.html");
//    }
}
/**
 * 配置退出处理器,解决登出后 principals 中还存在相应的 sessionInformation 的问题
 */
@Component
public class MyLogoutHandler implements LogoutHandler {

    @Autowired
    private ApplicationContext applicationContext;

    @Override
    public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
        String sessionId = request.getRequestedSessionId();
        if (Objects.nonNull(sessionId)) {
            SessionRegistry sessionRegistry = applicationContext.getBean(SessionRegistry.class);
            sessionRegistry.removeSessionInformation(sessionId);
        }
    }
}
/**
 * 退出成功处理器
 */
@Component
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {

    private ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        response.setContentType(ResponseContent.JSON_UTF8.getValue());
        response.setStatus(HttpStatus.OK.value());
        response.getWriter().write(objectMapper.writeValueAsString(AuthResponseIO.ok("退出成功!")));
    }
}

自定义状态码页面 

 @Configuration
    public class ErrorPageConfig implements ErrorPageRegistrar {
        @Override
        public void registerErrorPages(ErrorPageRegistry registry) {
            List<ErrorPage> errorPages = new ArrayList<>();
            errorPages.add(new ErrorPage(HttpStatus.NOT_FOUND, "/404.html"));
            errorPages.add(new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/500.html"));
            registry.addErrorPages(errorPages.toArray(new ErrorPage[0]));
        }
    }

 security bean 配置


 将需要注入的bean单独配置 

 使用密码加密参考:链接

@Configuration
public class SecurityBean {

    /**
     * HTTP 防火墙可防止HTTP动词篡改和跨站点跟踪
     * 参考 <a href="https://www.owasp.org/index.php/Test_HTTP_Methods_(OTG-CONFIG-006)">
     *
     * @return StrictHttpFirewall
     */
    @Bean
    public StrictHttpFirewall httpFirewall() {
        StrictHttpFirewall firewall = new StrictHttpFirewall();
        //HTTP方法白名单跨站跟踪(XST)和HTTP动词篡改
        firewall.setAllowedHttpMethods(Arrays.asList(HttpMethod.GET.name(), HttpMethod.POST.name()));
        return firewall;
    }

    /**
     * session会话注册表
     *
     * @return SessionRegistry
     */
    @Bean
    public SessionRegistry sessionRegistry() {
        return new SessionRegistryImpl();
    }

    @Autowired
    private SecurityProperties securityProperties;

    /**
     * 处理 rememberMe 自动登录
     */
    @Bean
    public PersistentTokenRepository persistentTokenRepository() {
        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
        // 获取配置的数据源
        DataSource dataSource = applicationContext.getBean(DataSource.class);
        jdbcTokenRepository.setDataSource(dataSource);
        //设置启动时创建表
        jdbcTokenRepository.setCreateTableOnStartup(securityProperties.isCreateTableOnStartup());
        return jdbcTokenRepository;
    }


    @Autowired
    private ApplicationContext applicationContext;

    /**
     * 密码策略为默认,为了安全希望自定义
     * 参考:PasswordEncoderFactories.createDelegatingPasswordEncoder();
     * 密码策略put的内容为都支持的算法,不安全的算法最好不要支持
     * @return PasswordEncoder
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

    /**
     * 重定向策略
     *
     * @return RedirectStrategy
     */
    @Bean
    public RedirectStrategy redirectStrategy() {
        return new DefaultRedirectStrategy();
    }

    /**
     * Dao认证提供商
     *
     * @return DaoAuthenticationProvider
     */
    @Bean
    public DaoAuthenticationProvider daoAuthenticationProvider() {
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setHideUserNotFoundExceptions(false);
        UserDetailsService userDetailsService = applicationContext.getBean(MyUserDetailsService.class);
        provider.setUserDetailsService(userDetailsService);
        provider.setPasswordEncoder(passwordEncoder());
        return provider;
    }

}

 UserDetails

@Getter
@Setter
public class MyUserDetails extends User {
    //以后第三方登录会使用
    private LoginType loginType = LoginType.NORMAL;

    private String userId;

    private String remoteAddress;

    public MyUserDetails(String username, String password, Collection<? extends GrantedAuthority> authorities) {
        super(username, password, authorities);
    }

    public MyUserDetails(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {
        super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
    }
}


@Getter
public enum LoginType {
    NORMAL, SMS, SOCIAL
}

userDetailsService

@Component
public class MyUserDetailsService implements UserDetailsService {

    @Autowired
    private ApplicationContext applicationContext;

    /**
     * 需要的数据参考{@link User#withUserDetails}
     *
     * @param username username
     * @return UserDetails
     * @throws UsernameNotFoundException e
     */
    @Override
    public UserDetails loadUserByUsername(String username) {
        //SysUser,SysRole,SysPermission为实体类
        //逻辑层SysUserService
        SysUserService sysUserService = applicationContext.getBean(SysUserService.class);
        //查询
        SysUser sysUser = sysUserService.findByLoginName(username);
        if (Objects.nonNull(sysUser)) {
             //获取角色
            Set<SysRole> sysRoles = sysUser.getSysRoles();
            String[] roleCode = new String[]{};
            String[] perCode = new String[]{};
            if (Objects.nonNull(sysRoles) && !sysRoles.isEmpty()) {
                //角色必须以ROLE_开头 例如ROLE_ADMIN
                roleCode = sysRoles.stream().filter(sysRole -> Objects.nonNull(sysRole.getRoleCode())).map(SysRole::getRoleCode).distinct().toArray(String[]::new);
                //具体的权限 看不懂参考jdk8-lambda表达式
                List<SysPermission> permissions = sysRoles.stream().
                        filter(sysRole -> Objects.nonNull(sysRole.getSysPermissions()) && !sysRole.getSysPermissions().isEmpty())
                        .map(SysRole::getSysPermissions).flatMap(Collection::stream).collect(Collectors.toList());
                perCode = permissions.stream().filter(o -> Objects.nonNull(o.getPerCode()))
                        .map(SysPermission::getPerCode).distinct().toArray(String[]::new);
            }
            List<GrantedAuthority> roles = AuthorityUtils.createAuthorityList(roleCode);
            List<GrantedAuthority> permissions = AuthorityUtils.createAuthorityList(perCode);
            roles.addAll(permissions);
            MyUserDetails myUserDetails = new MyUserDetails(sysUser.getLoginName(), sysUser.getPassword(),
                    sysUser.isEnabled(), sysUser.isAccountNonExpired(),
                    sysUser.isCredentialsNonExpired(), sysUser.isAccountNonLocked(),
                    roles);
            myUserDetails.setUserId(sysUser.getUserId());
            return myUserDetails;
        } else {
            throw new UsernameNotFoundException("未知用户异常!");
        }
    }
}

 


security config 配置


/**
 * 了解 跨站请求伪造(CSRF)攻击,动词篡改和跨站点跟踪
 */
@EnableWebSecurity
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableConfigurationProperties(SecurityProperties.class)
@Import({MyUserDetailsService.class, SecurityBean.class, MyAccessDeniedHandler.class, MyAuthenticationSuccessHandler.class,
        MyAuthenticationFailureHandler.class, MyLogoutHandler.class,
        MyLogoutSuccessHandler.class, MyInvalidSessionStrategy.class,
        MySessionInformationExpiredStrategy.class})
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private SecurityProperties securityProperties;

    /**
     * 配置忽略路径
     *
     * @param web WebSecurity
     * @throws Exception e
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers(securityProperties.getAnonResourcesUrl());
    }

    @Autowired
    private MyUserDetailsService myUserDetailsService;
    @Autowired
    private MyAccessDeniedHandler myAccessDeniedHandler;
    @Autowired
    private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
    @Autowired
    private MyAuthenticationFailureHandler myAuthenticationFailureHandler;
    @Autowired
    private MyLogoutHandler myLogoutHandler;
    @Autowired
    private MyLogoutSuccessHandler myLogoutSuccessHandler;
    @Autowired
    private MyInvalidSessionStrategy myInvalidSessionStrategy;
    @Autowired
    private MySessionInformationExpiredStrategy mySessionInformationExpiredStrategy;
    @Autowired
    private SessionRegistry sessionRegistry;
    @Autowired
    private PersistentTokenRepository persistentTokenRepository;

    /**
     * 权限认证
     *
     * @param http http
     * @throws Exception e
     * @<code> 也可以配置免认证资源:.antMatchers("/resources/**", "/public", "/about").permitAll()。permitAll为任何用户都可以请求访问 </code>
     * @<code> 登录跳转页面:loginPage() </code>
     * @<code> 登录成功跳转:successForwardUrl() </code>
     * @<code> 可选项:启用会话URL重写(默认false,慎用):enableSessionUrlRewriting()</code>
     * @<code> 页面使用iframe 禁用http.headers().frameOptions().disable()</code>
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        /*全部请求需认证*/
        http.authorizeRequests().anyRequest().authenticated()
                .and()
                /*访问被拒绝处理器*/
                .exceptionHandling().accessDeniedHandler(myAccessDeniedHandler)
                .and()
                /*登录*/
                .formLogin()
                .loginPage("/login.html")
                .successForwardUrl("index.html")
                .loginProcessingUrl(securityProperties.getLoginUrl())
                //处理登录成功
                .successHandler(myAuthenticationSuccessHandler)
                //处理登录失败
                .failureHandler(myAuthenticationFailureHandler)
                //任何用户都可以请求访问
                .permitAll()
                .and()
                /*添加记住我功能(自动登录)*/
                .rememberMe()
                .tokenRepository(persistentTokenRepository) // 配置 token 持久化仓库
                .tokenValiditySeconds((int) securityProperties.getSessionTimeout().getSeconds()) // rememberMe 过期时间,单为秒
                .userDetailsService(myUserDetailsService)
                .and()
                /*配置 session管理器*/
                .sessionManagement()
                //session过期策略
                .invalidSessionStrategy(myInvalidSessionStrategy)
                //session过期跳转url
                .invalidSessionUrl(securityProperties.getLoginUrl())
                //最大并发登录数量
                .maximumSessions(securityProperties.getMaximumSession())
                //最大会话阻止登录(第二次登录操作,默认false第二次登录踢出第一次,true禁止第二次登录)
                .maxSessionsPreventsLogin(false)
                //处理并发登录被踢出
                .expiredSessionStrategy(mySessionInformationExpiredStrategy)
                .expiredUrl(securityProperties.getLoginUrl())
                //配置session注册中心
                .sessionRegistry(sessionRegistry)
                .and().and()
                /*退出*/
                .logout()
                //退出时处理器
                .addLogoutHandler(myLogoutHandler)
                //退出成功处理器
                .logoutSuccessHandler(myLogoutSuccessHandler)
                .logoutUrl(securityProperties.getLogoutUrl())
                .logoutSuccessUrl(securityProperties.getLoginUrl())
                //使Http会话无效
                .invalidateHttpSession(true)
                .deleteCookies("JSESSIONID")
                .permitAll()
                .and()
                .httpBasic();
    }

    @Autowired
    private DaoAuthenticationProvider daoAuthenticationProvider;

    /**
     * 认证管理生成器
     *
     * @param auth 认证
     * @throws Exception e
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(daoAuthenticationProvider);
    }

}

  登录退出controller不需要配置


注解使用


@EnableGlobalMethodSecurity(prePostEnabled = true) 上面配置有开启

获取登录的信息

 public static MyUserDetails getCurrentUser() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        Object principal = authentication.getPrincipal();
        if (principal instanceof MyUserDetails) {
            return (MyUserDetails) principal;
        }
        return null;
    }

使用的是SpEL表达式访问Authentication.getPrincipal()

 

表达式 描述

hasRole([role])

true如果当前主体具有指定角色,则返回。默认情况下,如果提供的角色不以“ROLE_”开头,则会添加该角色。这可以通过修改打开defaultRolePrefix来自定义DefaultWebSecurityExpressionHandler

hasAnyRole([role1,role2])

返回true当前主体是否具有任何提供的角色(以逗号分隔的字符串列表给出)。默认情况下,如果提供的角色不以“ROLE_”开头,则会添加该角色。这可以通过修改打开defaultRolePrefix来自定义DefaultWebSecurityExpressionHandler

hasAuthority([authority])

true如果当前主体具有指定的权限,则返回。

hasAnyAuthority([authority1,authority2])

返回true如果当前主体具有任何所提供的当局的(给定为逗号分隔的字符串列表)

principal

允许直接访问代表当前用户的主体对象

authentication

允许直接访问从中获取的当前Authentication对象SecurityContext

permitAll

始终评估为 true

denyAll

始终评估为 false

isAnonymous()

true如果当前主体是匿名用户,则返回

isRememberMe()

返回true如果当前主要是记得,我的用户

isAuthenticated()

true如果用户不是匿名用户,则返回

isFullyAuthenticated()

返回true如果用户不是匿名或记得,我的用户

hasPermission(Object target, Object permission)

true如果用户有权访问给定权限的提供目标,则返回。例如,hasPermission(domainObject, 'read')

hasPermission(Object targetId, String targetType, Object permission)

true如果用户有权访问给定权限的提供目标,则返回。例如,hasPermission(1, 'com.example.domain.Message', 'read')

 

数据角色表role 添加 字段role_code 为ROLE_ADMIN 

包含的权限表permission添加字段per_code 为:user:create

 和用户配置下关系


 注意:在MyUserDetailsService配置中AuthorityUtils.createAuthorityList() 其实按官方是用来配置角色的,我配置为角色和权限。 这样使用有一个问题那就是权限修改无法同步到已登录用户的session中,解决参考下一篇: security JWT配置。


  • hasAuthority:

      可以读取全部的内容,hasAuthority读取角色需要写前缀

    @RequestMapping("test2")
    @PreAuthorize("hasAuthority('user:create')")
    public String t2(){
        return "菜单权限认证成功";
    }
@RequestMapping("test1")
    @PreAuthorize("hasAuthority('ROLE_ADMIN')")
    public String t(){
        return "角色权限认证成功";
    }

 


  • hasRole:

      读取角色必须以ROLE_开头认为是角色,所以角色读取去掉前缀。

    @RequestMapping("test1")
    @PreAuthorize("hasRole('ADMIN')")
    public String t(){
        return "角色权限认证成功";
    }

      还可以这样使用 @PreAuthorize("hasRole('USER') or hasRole('ADMIN')")


  • hasPermission:

  官方配置需要域对象安全性(ACL)读取权限,还需依赖

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

 使用起来比较麻烦采用自定义。

  1. 修改MyUserDetailsService

  /**
     * 需要的数据参考{@link User#withUserDetails}
     *
     * @param username username
     * @return UserDetails
     * @throws UsernameNotFoundException e
     */
    @Override
    public UserDetails loadUserByUsername(String username) {
        SysUserService sysUserService = applicationContext.getBean(SysUserService.class);
        SysUser sysUser = sysUserService.findByLoginName(username);
        if (Objects.nonNull(sysUser)) {
            Set<SysRole> sysRoles = sysUser.getSysRoles();
            String[] roleCode = new String[]{};
            if (Objects.nonNull(sysRoles) && !sysRoles.isEmpty()) {
                //数据库角色码必须以ROLE_开头
                roleCode = sysRoles.stream().filter(sysRole -> Objects.nonNull(sysRole.getRoleCode())).map(SysRole::getRoleCode).distinct().toArray(String[]::new);
            }
            MyUserDetails myUserDetails = new MyUserDetails(sysUser.getLoginName(), sysUser.getPassword(),
                    sysUser.isEnabled(), sysUser.isAccountNonExpired(),
                    sysUser.isCredentialsNonExpired(), sysUser.isAccountNonLocked(),
                    AuthorityUtils.createAuthorityList(roleCode));
            myUserDetails.setUserId(sysUser.getUserId());
            return myUserDetails;
        } else {
            throw new UsernameNotFoundException("未知用户异常!");
        }
    }
}

 


/**
 * hasPermission自定义注解 @PreAuthorize("hasPermission()")的hasPermission认证内容
 * fixme:使用注意修改 {@link MyUserDetailsService#loadUserByUsername(String)} 去掉权限码
 */
@Configuration
public class MyPermissionEvaluator implements PermissionEvaluator {

    @Autowired
    private ApplicationContext applicationContext;


    /**
     * 这样使用的会有一个问题就是每次需要查询(可以解决权限同步)
     *
     * @param authentication Authentication
     * @param prefix         前缀
     * @param suffix         后缀
     * @return boolean
     */
    @Override
    public boolean hasPermission(Authentication authentication, Object prefix, Object suffix) {
        SysUserService userService = applicationContext.getBean(SysUserService.class);
        boolean flag = false;
        //获取登录后校验的用户数据
        Object principal = authentication.getPrincipal();
        //判断是否是MyUserDetails的实例(会有其它类型的登录例如oauth2)
        if (principal instanceof MyUserDetails) {
            MyUserDetails userDetails = (MyUserDetails) principal;
            //查询
            SysUser user = userService.findByLoginName(userDetails.getUsername());
            if (Objects.nonNull(user)) {
                Set<SysRole> sysRoles = user.getSysRoles();
                if (Objects.nonNull(sysRoles) && !sysRoles.isEmpty()) {
                    List<String> perCodes = sysRoles.stream().map(SysRole::getSysPermissions)
                            .filter(Objects::nonNull).flatMap(Collection::stream)
                            .map(SysPermission::getPerCode).distinct().collect(Collectors.toList());
                    if (Objects.nonNull(perCodes) && !perCodes.isEmpty()) {
                        if (Objects.nonNull(prefix) && Objects.nonNull(suffix)) {
                            String code = prefix.toString() + ":" + suffix.toString();
                            for (String perCode : perCodes) {
                                if (Objects.equals(code, perCode)) {
                                    flag = true;
                                    break;
                                }
                            }
                        }
                    }
                }
            }

        }
        return flag;
    }

    //未使用调用返回false,认证失败
    @Override
    public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {
        return false;
    }
}

例如:

    @RequestMapping("test3")
    @PreAuthorize("hasPermission('user','create')")
    public String t3(){
        return "认证成功";
    }


下一篇: security JWT配置(暂更

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章