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配置(暫更

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