Spring Security + Spring Security Oauth2

Spring Security

pom.xml

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--thymeleaf对security的支持-->
<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity5</artifactId>
    <version>${thymeleaf-extras-springsecurity5-version}</version>
</dependency>

Web端:

WebSecurityConfig

主要端配置类:WebSecurityConfig,在这个文件中可以配置以下

  • loginPage 登录页面的地址(如果没有提供,Spring Security提供了默认的登录界面)

  • handler:

    • successHandler
    • authenticationFailureUrl
    • logoutSuccessHandler
  • session处理相关配置
  • 授权管理
  • 配置新的拦截器

WebSecurityConfig.java

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true, jsr250Enabled=true, prePostEnabled = true)// 控制权限注解 配合 @Secured({"ROLE_ADMIN","ROLE_USER2"})使用
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private ValidateFilter validateFilter;

    @Autowired
    private AuthenticationSuccessHandler authenticationSuccessHandler;

    @Autowired
    private AuthenticationFailureHandler authenticationFailureHandler;

    @Autowired
    private LogoutSuccessHandler logoutSuccessHandler;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.headers().disable()
                .authorizeRequests()
                .antMatchers("/forbidden", "/kaptcha").permitAll()
                .and()
                .authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/login")
                .successHandler(authenticationSuccessHandler)
                .failureHandler(authenticationFailureHandler)
                .permitAll()
                .and()
                .logout()
                .logoutSuccessHandler(logoutSuccessHandler)
                .permitAll()
                .and()
                .csrf().disable()// csrf会拦截POST请求 https://www.jianshu.com/p/2c275c75c77a
                .sessionManagement().
                    invalidSessionUrl("/login")
                    .maximumSessions(1)
                    .expiredSessionStrategy(new AcExpiredSessionStrategy())
                    .sessionRegistry(sessionRegistry());


        http.addFilterBefore(validateFilter, UsernamePasswordAuthenticationFilter.class);
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        //解决静态资源被拦截的问题
        web.ignoring().antMatchers("/coreui/**", "/css/**", "/js/**", "/img/**", "/plugins/**", "/favicon.ico");
    }

    @Bean
    public SessionRegistry sessionRegistry() {
        return new SessionRegistryImpl();
    }
}

PasswordEncoder

密码加密处理
pom.xml

@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

用户认证服务UserDetailsService

@Component
@Slf4j
public class AcUserDetailsService implements UserDetailsService {

    @Autowired
    private UserService userService;

    @Autowired
    private CacheManager cacheManager;

    @Autowired
    private SessionRegistry sessionRegistry;


    /**
     * 进行认证授权的工作
     *
     * @param username
     * @return
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String username) {
        username = username.toLowerCase();
        Cache tryCache = cacheManager.getCache("loginMaxTry");

        Object object = tryCache.get(username);
        int loginMaxTryCount = 1;
        if (Objects.isNull(object)) {
            tryCache.put(username, loginMaxTryCount);
        } else {
            loginMaxTryCount = (Integer) object + 1;
            tryCache.put(username, loginMaxTryCount);
        }

        if (loginMaxTryCount > AuthConstants.MAX_TRY_COUNT) {
            throw new MaxTryLoginException();
        }

        UserDTO user;

        try {
            user = userService.findByUsername(username);
        } catch (DataAccessException e) {
            throw new DataAccessResourceFailureException("数据库连接出错, 请联系管理员");
        }

        if (user == null) {
            throw new UsernameNotFoundException("帐号不存在或者账号被锁定");
        }

        if (isLogin(username)) {
            log.info("LOGIN: {}已经登录,踢出登录", username );
        }

        return new AcUserDetails(user, AuthorityUtils.createAuthorityList(user.getRoles().toArray(new String[]{})));
    }

    /**
     * 用户是否处于登录状态
     * @param username
     * @return
     */
    private boolean isLogin(String username) {
        List<Object> list = sessionRegistry.getAllPrincipals();
        for (Object o : list) {
            if (StringUtils.equals(((AcUserDetails)o).getUsername(), username)) {
                return sessionRegistry.getAllSessions(o, false).size() > 0;
            }

        }

        return false;
    }
}

Spring Security OAuth2

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

<dependency>
    <groupId>org.springframework.security.oauth.boot</groupId>
    <artifactId>spring-security-oauth2-autoconfigure</artifactId>
    <version>2.1.3.RELEASE</version>
</dependency>

在Spring Security的基础上多次两个概念
* @EnableAuthorizationServer 授权服务器
* @EnableResourceServer 资源服务器

如果授权服务器和资源服务器部署「在」同一台服务器上

  • 采用授权码模式会有问题,因为采用授权码模式需要跳转「登录」界面,需要使用WebSecurityConfig。WebSecurityConfig和@EnableResourceServer会有冲突。

  • 采用密码模式没有问题。不需要依赖WebSecurityConfig,因为不依赖WebSecurityConfig,需要自己构造AuthenticationManager,测试时否则会报异常。


@Bean public AuthenticationManager authenticationManager(UserDetailsService userDetailsService, PasswordEncoder passwordEncoder) { List<AuthenticationProvider> providers = new ArrayList<>(); DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider(); daoAuthenticationProvider.setUserDetailsService(userDetailsService); daoAuthenticationProvider.setPasswordEncoder(passwordEncoder); providers.add(daoAuthenticationProvider); return new ProviderManager(providers); }

如果授权服务器和资源服务器部署「不在」同一台服务器上

  • 授权服务器

    • WebSecurityConfig + @EnableAuthorizationServer 授权码和密码模式
    • @EnableAuthorizationServer 密码模式
  • 资源服务器
    @EnableResourceServer

授权服务器

@Configuration
@EnableAuthorizationServer
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    @Qualifier("tokenServices")
    private AuthorizationServerTokenServices tokenServices;

    @Autowired
    private AuthorizationCodeServices authorizationCodeServices;

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security
                .tokenKeyAccess("permitAll()")                    //oauth/token_key是公开
                .checkTokenAccess("permitAll()")//oauth/check_token公开
                .allowFormAuthenticationForClients()                //密码模式:表单认证(申请令牌)
        ;
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
                clients.inMemory()// 使用in-memory存储
                .withClient("c1")// client_id
                .secret(passwordEncoder.encode("secret"))//客户端密钥
                .resourceIds("res1")//资源列表
                .authorizedGrantTypes("authorization_code", "password", "client_credentials", "implicit", "refresh_token")// 该client允许的授权类型authorization_code,password,refresh_token,implicit,client_credentials
                .scopes("all")// 允许的授权范围
                .autoApprove(false)//false跳转到授权页面
                //加上验证回调地址
                .redirectUris("http://www.baidu.com");
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints
                .userDetailsService(userDetailsService)
                .authenticationManager(authenticationManager)// 认证管理器 => 密码模式需要在认证服务器中设置 中配置AuthenticationManager
                .authorizationCodeServices(authorizationCodeServices)//授权码服务
                .tokenServices(tokenServices)// 令牌管理服务
                .allowedTokenEndpointRequestMethods(HttpMethod.POST);
    }


资源服务器

@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

    public static final String RESOURCE_ID = "res1";

    @Autowired
    private TokenStore tokenStore;

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
        // 资源id
        resources.resourceId(RESOURCE_ID)
                .tokenStore(tokenStore)
                .stateless(true)
                .accessDeniedHandler((request, response, e) -> {
                    response.getWriter().write("look=>" + e);
                });
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeRequests()
                .antMatchers("/order/r2").hasAnyAuthority("p2")
                .antMatchers("/login*").permitAll()
                .anyRequest().authenticated()
                // 需要身份认证的时候跳转的URL(可以直接指定html)
        ;
    }
}

关于html table 固定列宽

.t3 {
            width: 100%;
            table-layout: fixed; 
        }

        .t3 td, .t3 th {
            white-space: nowrap;
            word-break: break-all;
            overflow: hidden;
            text-overflow: ellipsis;
        }
  • t1未经修饰的table;t2百分百宽度;t3固定列宽,文字过多显示省略号,如果table超过容器宽度,有滚动条。

完整代码如下。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <style>
        .container {
            width: 500px;
            background: #ccc;
            margin: 0 auto;
            overflow: auto;
        }

        table , table td, table, th{
            border: 1px solid red;
            border-collapse: collapse;
        }

        .t1 {}

        .t2 {
            width: 100%;
        }

        .t3 {
            width: 100%;
            table-layout: fixed; 
        }

        .t3 td, .t3 th {
            white-space: nowrap;
            word-break: break-all;
            overflow: hidden;
            text-overflow: ellipsis;
        }
    </style>
</head>
<body>
    <div class="container">
        <h2>t1原始table</h2>
        <table class="t1">
            <thead>
                <tr>
                    <th>hello</th>
                    <th>world</th>
                </tr>
            </thead>
            <tbody>
                <tr>
                    <td>faff</td>
                    <td>faff</td>
                </tr>
            </tbody>
        </table>
    <br>
    <h2>t2宽度100%</h2>
        <table class="t2">
            <thead>
                <tr>
                    <th>hello</th>
                    <th>world</th>
                </tr>
            </thead>
            <tbody>
                <tr>
                    <td>faff</td>
                    <td>faff</td>
                </tr>
            </tbody>
        </table>
<br>
<h2>t3有滚动条</h2>
        <table class="t3">
            <thead>
                <tr>
                    <th style="width: 400px;">400px</th>
                    <th style="width: 120px;">120px120px120px120px120px120px</th>
                </tr>
            </thead>
            <tbody>
                <tr>
                    <td>400px400px400px400px400px400px400px400px400px400px400px400px400px400px400px400px400px400px400px400px400px400px</td>
                    <td>dfasfsdfasfsdfasfsdfasfsdfasfsdfasfsdfasfsdfasfsdfasfsdfasfsdfasfsdfasfs</td>
                </tr>
            </tbody>
        </table>

    </div>
</body>
</html>
  • 运行截图
    t

js动态创建css样式

function setCssText(css){ 
    if(document.all){ // document.createStyleSheet(url)
        window.style = css
        document.createStyleSheet("javascript:style")
    }else{ //document.createElement(style)
        var style = document.createElement('style')
        style.type = 'text/css'
        style.innerHTML = css
        document.getElementsByTagName('HEAD').item(0).appendChild(style)
    } 
}
setCssText('body { color: red }')

position:sticky实现特殊的业务需求

这是一个结合了 position:relative 和 position:fixed 两种定位功能于一体的特殊定位,适用于一些特殊场景。

当父元素部分滚动到”视线范围外”时,开始实现「fixed」的功能。当父元素整个不在“实现范围内”,取消「fixed」的功能。


<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>sticky</title> <style> .parent { display: flex; /*注释这个瞧瞧瞧瞧*/ } .left { background: red; height: 500px; width: 220px; position: sticky; top: 20px; /*须指定 top, right, bottom 或 left 四个阈值其中之一,才可使粘性定位生效*/ z-index: -1; } .right { background: blue; height: 1200px; flex: 1; } </style> </head> <body> <div style="height: 200px;"></div> <div class="parent"> <div class="left"> </div> <div class="right"> </div> </div> <div style="height: 1200px;"></div> </body> </html>

利用background-attachment:fixed做页面效果

background-attachment:

  • scroll 默认值。背景图像会随着页面其余部分的滚动而移动。
  • fixed 当页面的其余部分滚动时,背景图像不会移动。
    <section class="banner">

    </section>

    <div class="content">

    </div>
    .banner {
        background-image: url(banner.jpg);
        background-size: cover;
        background-attachment: fixed;
        height: 400px;
    }

    .content {
        height: 1000px;
        background-color: red;
    }