SpringSecurity

SpringSecurity

_

依赖

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

config

继承 WebSecurityConfigurerAdapter

@Component
public class LoginSecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 重写configure(HttpSecurity http)
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
                //自定义的登录页(通过controller跳转到login.html)
                .loginPage("/toLogin")
                //登录请求路径
                .loginProcessingUrl("/login")
		//成功跳转页面
                .successForwardUrl("/")
		//失败跳转
                .failureForwardUrl("/toError");

        http.authorizeRequests()
                //放行资源
                .antMatchers("/toLogin").permitAll()
                //所有资源需要认证才能登录
                .anyRequest().authenticated();

        http.csrf().disable();
    }


    //密码加密  官方推荐使用BCryptPasswordEncoder 
    @Bean
    public PasswordEncoder getPasswordEncoder(){
        return new BCryptPasswordEncoder();
    }
}

登录逻辑

实现 UserDetailsService 接口

重写 loadUserByUsername(String username) 方法

@Service
@Slf4j
public class LoginDetailsServiceImpl implements UserDetailsService {

    @Resource
    private PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        log.info("LoginDetailsServiceImpl.loadUserByUsername");
        //登录逻辑  一般是从数据库拿数据,
        if (!"admin".equals(username)){
            //在数据查不到此用户,抛出异常
            throw  new UsernameNotFoundException("查无此人");
        }
        //将密码加密 一般从数据库拿 ,数据库存的加密后的密码,这里加密后对比一下,一样==登录成功
        String password = passwordEncoder.encode("admin");
        log.info("用户={},密码={}",username,password);

        //登录成功返回security的User对象 ,并传入用户名,密码和权限
        return new User(username,password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin,user"));
    }
}

自定义登录成功和失败处理器

分别需要实现 AuthenticationSuccessHandlerAuthenticationFailureHandler 接口

public class MySuccessForwardUrl implements AuthenticationSuccessHandler {

    private String url;

    MySuccessForwardUrl(String url){
        this.url=url;
    }

    @Override
    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        //登录成功则重定向到百度
        httpServletResponse.sendRedirect(url);
    }
}
public class MyFailureForwardUrl implements AuthenticationFailureHandler {

    /**
     * 要跳转的url
     */
    private String url;

    MyFailureForwardUrl(String url){
        this.url=url;
    }

    @Override
    public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        //失败转发到错误页面
        httpServletRequest.getRequestDispatcher(url).forward(httpServletRequest,httpServletResponse);
    }
}
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
                //自定义的登录页(通过controller跳转到login.html)
                .loginPage("/toLogin")
                //登录请求路径
                .loginProcessingUrl("/login")
//                .successForwardUrl("/")
                .successHandler(new MySuccessForwardUrl("https://www.baidu.com"))
//                .failureForwardUrl("/toError");
                .failureHandler(new MyFailureForwardUrl("/toError"));

        http.authorizeRequests()
                //放行资源
                .antMatchers("/toLogin").permitAll()
                //所有资源需要认证才能访问
                .anyRequest().authenticated();

        http.csrf().disable();
    }

放行静态资源

        http.authorizeRequests()
                //放行资源
                .antMatchers("/toLogin").permitAll()
                //放行静态资源  简单写写
                .antMatchers("/js/**","/css/**").permitAll()
                //所有资源需要认证才能登录
                .anyRequest().authenticated();

权限和角色

权限配置:.antMatchers(“/admin”).hasAnyAuthority(“admin”)

角色配置: .antMatchers(“/admin”).hasRole(“abc”)

直接在后面加

//权限配置
.antMatchers("/admin").hasAnyAuthority("admin")
 //角色配置
.antMatchers("/admin").hasRole("abc")

角色必须加上 ROLE_ 前缀

@Service
@Slf4j
public class LoginDetailsServiceImpl implements UserDetailsService {

    @Resource
    private PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        log.info("LoginDetailsServiceImpl.loadUserByUsername");
        //登录逻辑  一般是从数据库拿数据,
        if (!"admin".equals(username)){
            //在数据查不到此用户,抛出异常
            throw  new UsernameNotFoundException("查无此人");
        }
        //将密码加密 一般从数据库拿 ,数据库存的加密后的密码,这里加密后对比一下,一样久等了成功
        String password = passwordEncoder.encode("admin");
        log.info("用户={},密码={}",username,password);

        //登录成功返回security的User对象 ,并传入用户名,密码和权限
	// 角色必须加上 ROLE_ 前缀
        return new User(username,password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin,user,ROLE_abc"));
    }
}

ip限定

//只有此ip可以访问
.antMatchers("/admin").hasIpAddress("127.0.0.1")

自定义403

public class MyAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
        httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN);
        httpServletResponse.setHeader("Content-Type","application/json;charset=utf-8");
        PrintWriter writer = httpServletResponse.getWriter();
        writer.write("{\"status\":\"error\",\"msg\":\"权限不住,请联系管理员\"}");
        writer.flush();
        writer.close();
    }
}
http.exceptionHandling().accessDeniedHandler(new MyAccessDeniedHandler());

注解开发

使用 @EnableGlobalMethodSecurity 开启security注解模式

开启后如果程序没有权限等会报500

org.springframework.security.access.AccessDeniedException

注解 作用
@EnableGlobalMethodSecurity 开启security注解模式
@Secured() 角色判断,参数要以 ROLE_ 开头
@PreAuthorize() 方法或类在执行之前先判断权限
@PostAuthorize() 方法或类在执行结束后判断权限

@EnableGlobalMethodSecurity

@EnableGlobalMethodSecurity 默认全是false,所以要使用要手动开启

@SpringBootApplication
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class BootSecurityApplication {

    public static void main(String[] args) {
        SpringApplication.run(BootSecurityApplication.class, args);
    }

}

@Secured

    @Secured("ROLE_abc")
    @RequestMapping("/admin")
    @ResponseBody
    public String admin(){
        return "admin";
    }

}

@PreAuthorize

@PreAuthorize 可以验证权限也可以验证角色

@PreAuthorize允许以 ROLE_开头

//    @PreAuthorize("admin")
    @PreAuthorize("hasRole('admin')")
//  @PreAuthorize("hasRole('ROLE_admin')")
    @RequestMapping("/admin")
    @ResponseBody
    public String admin(){
        return "admin";
    }

}

@PostAuthorize作用一样,只是执行顺序不一样,但是这个注解不常用。

RememberMe功能实现

RememberMe --> 记住我功能

用户只需要在登录时添加 remember-me复选框,取值为true,stringsecurity会自动把用户信息存储到数据源中,以后就可以不等录进行访问。

存入数据库 导入mybatis-plus和数据库链接依赖

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.2</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

记住我配置

        //记住我
        http.rememberMe()
                //失效时间秒
                .tokenValiditySeconds(60)
                //自定义登录逻辑
                .userDetailsService(detailsService)
                //持久层对象
                .tokenRepository(persistentTokenRepository);
    }

配置持久层

    //在yaml中配置数据源,注入数据源
    @Resource
    private DataSource dataSource;

     //将配置的getPersistentTokenRepository注入
    @Resource
    private PersistentTokenRepository persistentTokenRepository;

    @Bean
    public PersistentTokenRepository getPersistentTokenRepository(){
        //PersistentTokenRepository的实现类JdbcTokenRepositoryImpl
        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
        //设置数据源
        jdbcTokenRepository.setDataSource(dataSource);
        //第一次启动的时候建表,后面要关闭,不然表存在了再去创建会报错
        jdbcTokenRepository.setCreateTableOnStartup(true);
        return jdbcTokenRepository;
    }

html

<form th:action="@{/login}" method="post">
    <input type="text" name="username" placeholder="用户名"/><br/>
    <input type="text" name="password" placeholder="密码"/><br/>
    <!--  name必须为remember-me  value为true  -->
    记住我:<input type="checkbox" name="remember-me" value="true"/><br/>
    <input type="submit" value="登录"/>
</form>

退出登录

http.logout().logoutUrl("/toLogin");
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>首页</title>
</head>
<body>
hello
<a href="/logout">退出</a>
</body>
</html>

csrf

csrf 就是跨站请求伪造,通过伪造用户请求访问受信任站点的非法请求

springsecuruty默认开启 csrf,默认会拦截请求,为了保证不是其他第三方网站访问,要求访问是携带参数_csrf值为token(服务端产生)的内容,如果token和服务端匹配成功,则正常访问。

删除前面的 http.csrf().disable();

<form th:action="@{/login}" method="post">
    <!-- th:value="${_csrf.token}"从服务端拿到这个值 name必须为 _csrf th:if 如果有值就放进去,没有就为null -->
    <input type="hidden" th:value="${_csrf.token}" name="_csrf" th:if="_csrf">
    <input type="text" name="username" placeholder="用户名"/><br/>
    <input type="text" name="password" placeholder="密码"/><br/>
    <!--  name必须为remember-me  value为true  -->
    记住我:<input type="checkbox" name="remember-me" value="true"/><br/>
    <input type="submit" value="登录"/>
</form>

Oauth2

~

单例模式 2021-04-10
常用正则 2021-04-16

评论区