Spring Boot 整合 Spring Security(详细学习笔记)


目录

  • Spring Boot 整合 Spring Security(详细学习笔记)
  • 一、什么是 Spring Security ?
  • 二、简单的入门案例
  • 1、引入 Spring Security 依赖
  • 2、创建 HelloController 类
  • 3、运行
  • 三、3个重要的过滤器
  • 1、FilterSecurityInterceptor 过滤器
  • 2、ExceptionTranslationFilter 过滤器
  • 3、UsernamePasswordAuthenticationFilter 过滤器
  • 4、Spring Security 中的过滤器是如何加载?
  • 4.1、配置 DelegatingFilterProxy 过滤器
  • 4.1.1、DelegatingFilterProxy 源码
  • 4.1.2、FilterChainProxy 源码
  • 四、2个重要的接口
  • 1、UserdetailsService 接口
  • 2、PasswordEncoder 接口
  • 五、Web 权限方案
  • 1、设置登录的用户名和密码
  • 1.1、方法一:通过配置文件
  • 1.2、方法三:通过配置类
  • 1.3、方法三:自定义编写实现类
  • 1.3.1、创建配置类,设置使用哪个 UserDetailsService 实现类
  • 1.3.2、编写实现类,返回User对象,User对象有用户名和操作权限
  • 1.3.3、问题总结
  • 六、实现查询数据库认证用户来完成用户登录
  • 1、引入依赖
  • 1.1、MySql依赖
  • 1.2、MyBatisPlus 依赖
  • 2、创建数据库和数据表
  • 3、创建 User 对象实体类
  • 4、整合mp,创建 UserMapper 接口,继承 BaseMapper 接口
  • 5、在 MyUserDetailService 中调用 UserMapper 里面的方法查询数据库
  • 6、在启动类上添加注解 @MapperScan
  • 七、自定义登录页面
  • 1、创建相关页面和Controller
  • 2、在配置类实现相关配置
  • 八、基于角色或权限进行控制
  • 1、hasAuthority 方法
  • 1.1、在配置类中当前地址有哪些权限在配置类实现相关配置
  • 1.2、在 MyUserDetailService 类中设置用户权限
  • 2、hasAnyAuthority 方法
  • 3、hasRole 方法
  • 4、hasAnyRole 方法
  • 九、自定义403页面
  • 9.1、编写403页面
  • 9.2、配置类中配置
  • 十、注解的使用
  • 10.1、@Secured 注解
  • 10.1.1、开启注解功能
  • 10.1.2、使用 @Secured 注解
  • 10.2、@PreAuthorize 注解
  • 10.2.1、开启注解功能
  • 10.2.2、使用 @PreAuthorize 注解
  • 10.3、@PostAuthorize 注解
  • 10.4、@PreFilter 注解 和 @PostFilter 注解
  • 10.4.1、@PreFilter 注解
  • 10.4.2、@PostFilter注解
  • 十一、用户注销
  • 11.1、在配置类中配置
  • 11.2、编写 home.html 页面和 Contrller
  • 11.2、测试过程
  • 十二、实现自动登录(基于数据库实现记住我)
  • 12.1、创建表
  • 12.2、在配置类中配置


一、什么是 Spring Security ?

Spring Security是一个功能强大且高度可定制的身份验证和访问控制框架,提供了完善的认证机制和方法级的授权功能。本质是一组过滤器链,不同的功能经由不同的过滤器。

二、简单的入门案例

1、引入 Spring Security 依赖

由于 Spring Boot 提供了 Maven BOM 表来管理依赖项版本,因此无需指定版本。

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

2、创建 HelloController 类

@RestController
@RequestMapping("/user")
public class HelloController {
    @GetMapping("say")
    public String say() {
        return "hello security!!!!";
    }
}

3、运行

通过浏览器访问 localhost:8080/user/say

跳转到 localhost:8080/login

默认用户名为 user

密码在启动的时候打印在控制台上:

Using generated security password: 04891b2b-a615-46cb-9093-ea5d8d96059a

验证完用户后,过滤器放行,跳转到 localhost:8080/user/say ,即可看到 hello security!!!。

三、3个重要的过滤器

Spring Security 本质是一个过滤链。

1、FilterSecurityInterceptor 过滤器

一个方法级的权限过滤器,基本位于过滤链的最低端。

public class FilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {
	......
        
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        this.invoke(new FilterInvocation(request, response, chain));
    }
        
    public void invoke(FilterInvocation filterInvocation) throws IOException, ServletException {
        if (this.isApplied(filterInvocation) && this.observeOncePerRequest) {
            filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
        } else {
        if (filterInvocation.getRequest() != null && this.observeOncePerRequest) {
                    filterInvocation.getRequest().setAttribute("__spring_security_filterSecurityInterceptor_filterApplied", Boolean.TRUE);
        }
		
        //看之前的过滤器是否放行
        InterceptorStatusToken token = super.beforeInvocation(filterInvocation);

        try {
        filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
        } finally {
            super.finallyInvocation(token);
        }
            super.afterInvocation(token, (Object)null);
        }
    }
    
    ......
}

2、ExceptionTranslationFilter 过滤器

一个异常过滤器,用来处理在认证授权过程中抛出异常。

public class ExceptionTranslationFilter extends GenericFilterBean implements MessageSourceAware {
    ......
    
    private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        try {
            chain.doFilter(request, response);
        } catch (IOException var7) {
            throw var7;
        } catch (Exception var8) {
            Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(var8);
            RuntimeException securityException = (AuthenticationException)this.throwableAnalyzer.getFirstThrowableOfType(AuthenticationException.class, causeChain);
            if (securityException == null) {
                securityException = (AccessDeniedException)this.throwableAnalyzer.getFirstThrowableOfType(AccessDeniedException.class, causeChain);
            }

            if (securityException == null) {
                this.rethrow(var8);
            }

            if (response.isCommitted()) {
                throw new ServletException("Unable to handle the Spring Security Exception because the response is already committed.", var8);
            }

            this.handleSpringSecurityException(request, response, chain, (RuntimeException)securityException);
        }

    }
        
    ......
}

3、UsernamePasswordAuthenticationFilter 过滤器

/login 的POST请求做拦截,校验表单中的用户名和密码。

public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
    ......
        
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if (this.postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        } else {
            String username = this.obtainUsername(request);
            username = username != null ? username : "";
            username = username.trim();
            String password = this.obtainPassword(request);
            password = password != null ? password : "";
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
            this.setDetails(request, authRequest);
            return this.getAuthenticationManager().authenticate(authRequest);
        }
    }

    ......
}

在入门中使用的是默认用户名,而实际中需要查数据库,后面会说到。

4、Spring Security 中的过滤器是如何加载?

4.1、配置 DelegatingFilterProxy 过滤器

入门案例中使用的是 Spring Boot ,而 Spring Boot 会自动配置 Spring Security。

一般情况下,推荐如下的组合(仅是推荐,无论怎样组合都能运行):

  • SSM + Shiro
  • Spring Boot / Spring Cloud + Spring Security
4.1.1、DelegatingFilterProxy 源码
public class DelegatingFilterProxy extends GenericFilterBean {
    ......
        
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        Filter delegateToUse = this.delegate;
        if (delegateToUse == null) {
            synchronized(this.delegateMonitor) {
                delegateToUse = this.delegate;
                if (delegateToUse == null) {
                    WebApplicationContext wac = this.findWebApplicationContext();
                    if (wac == null) {
                        throw new IllegalStateException("No WebApplicationContext found: no ContextLoaderListener or DispatcherServlet registered?");
                    }
					
                    //初始化
                    delegateToUse = this.initDelegate(wac);
                }

                this.delegate = delegateToUse;
            }
        }

        this.invokeDelegate(delegateToUse, request, response, filterChain);
    } 
    
    protected Filter initDelegate(WebApplicationContext wac) throws ServletException {
        //获得目标的名称:FilterChainProxy
        String targetBeanName = this.getTargetBeanName();
        Assert.state(targetBeanName != null, "No target bean name set");
        //通过容器获得对象
        Filter delegate = (Filter)wac.getBean(targetBeanName, Filter.class);
        if (this.isTargetFilterLifecycle()) {
            //初始化
            delegate.init(this.getFilterConfig());
        }

        return delegate;
    }
    
    ......
}
4.1.2、FilterChainProxy 源码
public class FilterChainProxy extends GenericFilterBean {
    ......
        
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        boolean clearContext = request.getAttribute(FILTER_APPLIED) == null;
        if (!clearContext) {
            this.doFilterInternal(request, response, chain);
        } else {
            try {
                request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
                //*
                this.doFilterInternal(request, response, chain);
            } catch (RequestRejectedException var9) {
                this.requestRejectedHandler.handle((HttpServletRequest)request, (HttpServletResponse)response, var9);
            } finally {
                SecurityContextHolder.clearContext();
                request.removeAttribute(FILTER_APPLIED);
            }

        }
    }
    
    private void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        FirewalledRequest firewallRequest = this.firewall.getFirewalledRequest((HttpServletRequest)request);
        HttpServletResponse firewallResponse = this.firewall.getFirewalledResponse((HttpServletResponse)response);
        //获得过滤链
        List<Filter> filters = this.getFilters((HttpServletRequest)firewallRequest);
        if (filters != null && filters.size() != 0) {
            if (logger.isDebugEnabled()) {
                logger.debug(LogMessage.of(() -> {
                    return "Securing " + requestLine(firewallRequest);
                }));
            }

            FilterChainProxy.VirtualFilterChain virtualFilterChain = new FilterChainProxy.VirtualFilterChain(firewallRequest, chain, filters);
            virtualFilterChain.doFilter(firewallRequest, firewallResponse);
        } else {
            if (logger.isTraceEnabled()) {
                logger.trace(LogMessage.of(() -> {
                    return "No security for " + requestLine(firewallRequest);
                }));
            }

            firewallRequest.reset();
            chain.doFilter(firewallRequest, firewallResponse);
        }
    }
    
     private List<Filter> getFilters(HttpServletRequest request) {
        int count = 0;
         //迭代器
        Iterator var3 = this.filterChains.iterator();

        SecurityFilterChain chain;
        do {
            if (!var3.hasNext()) {
                return null;
            }

            chain = (SecurityFilterChain)var3.next();
            if (logger.isTraceEnabled()) {
                ++count;
                logger.trace(LogMessage.format("Trying to match request against %s (%d/%d)", chain, count, this.filterChains.size()));
            }
        } while(!chain.matches(request));

        return chain.getFilters();
    }
    
    ......
}

四、2个重要的接口

1、UserdetailsService 接口

当什么都没有配置的时候,账号和密码是由 Spring Security 定义生成的。在实际情况中,账号和密码都是从数据库中查询出来的,所以需要自定义控制认证逻辑。

如果只需要自定义逻辑时,只需要实现 UserdetailsService 接口即可。接口定义如下:

public interface UserDetailsService {
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

返回 UserDetails 对象:

public interface UserDetails extends Serializable {
    Collection<? extends GrantedAuthority> getAuthorities();

    String getPassword();

    String getUsername();

    //判断账户是否过期
    boolean isAccountNonExpired();

    //判断账户是否被锁定
    boolean isAccountNonLocked();

    //判断凭证(密码)是否过期
    boolean isCredentialsNonExpired();
	
    //判断当前用户是否可用
    boolean isEnabled();
}

2、PasswordEncoder 接口

数据加密的接口,用于返回User对象里面密码加密。

public interface PasswordEncoder {
    //表示把参数按照特定的解析规则进行解析
    String encode(CharSequence rawPassword);

    //表示验证从存储中获取的编码密码与编码后提交的原始密码是否匹配。
    //匹配返回 true ,反之返回 false。
    //第一个参数表示需要被解析的密码,第二个参数表示存储的密码。
    boolean matches(CharSequence rawPassword, String encodedPassword);

    //表示解析的密码能够再次进行解析且达到更安全的结果则返回 true
    default boolean upgradeEncoding(String encodedPassword) {
        return false;
    }
}

五、Web 权限方案

  • 认证
  • 授权

1、设置登录的用户名和密码

1.1、方法一:通过配置文件

在 application.properties 中配置:

spring.security.user.name=lishi
spring.security.user.password=123456
1.2、方法三:通过配置类
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        String password = passwordEncoder.encode("123");
        auth.inMemoryAuthentication().withUser("zhangsan").password(password).roles("admin");
    }

    @Bean
    PasswordEncoder password() {
        return new BCryptPasswordEncoder();
    }
}
1.3、方法三:自定义编写实现类

上面两种方式在实际情况中并不实用,因为用户名和密码实际是通过查询数据库得到。

1.3.1、创建配置类,设置使用哪个 UserDetailsService 实现类
@Service("userDetailsService")
public class MyUserDetailService implements UserDetailsService {
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("role");
        return new User("wangwu",new BCryptPasswordEncoder().encode("123"),auths);
    }
}
1.3.2、编写实现类,返回User对象,User对象有用户名和操作权限
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    UserDetailsService userDetailsService;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(password());
    }

    @Bean
    PasswordEncoder password() {
        return new BCryptPasswordEncoder();
    }
}
1.3.3、问题总结

在学习过程中,第三种方式存在没有校验用户名的问题,即无论输入什么用户名,只要密码为123,就能够通过认证。

通过调试可以看出:

@Override
//假设在页面输入用户名为 songshu ,则该方法的参数的值也为 songhu
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
	List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("role");
	return new User("wangwu",new BCryptPasswordEncoder().encode("123"),auths);
}

通过 loadUserByUsername 方法名可以知其意为通过用户名加载 User 对象,这里是写死的,直接返回一个User对象,所以无法验证用户名,实际应该是通过用户名去数据库中查询用户数据封装到 User 中,而在这一步也就是对用户名的验证,只有通过用户名才能获得对应的密码,封装成 User 对象,最后与页面输入的密码进行校验。

六、实现查询数据库认证用户来完成用户登录

1、引入依赖

1.1、MySql依赖
<dependency>
	<groupId>mysql</groupId>
	<artifactId>mysql-connector-java</artifactId>
</dependency>
1.2、MyBatisPlus 依赖
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.0.5</version>
</dependency>

2、创建数据库和数据表

CREATE TABLE `student`  (
  `id` int NOT NULL COMMENT 'ID',
  `username` varchar(20) NOT NULL COMMENT '姓名',
  `password` varchar(20) NOT NULL COMMENT '密码',
  PRIMARY KEY (`id`)
);

3、创建 User 对象实体类

@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class User {
    private int id;
    private String username;
    private String password;
}

4、整合mp,创建 UserMapper 接口,继承 BaseMapper 接口

@Repository
public interface UserMapper extends BaseMapper<User> {
}

5、在 MyUserDetailService 中调用 UserMapper 里面的方法查询数据库

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

    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        QueryWrapper<com.songshu.voluntarylabour.pojo.User> wrapper = new QueryWrapper();
        wrapper.eq("username",username);
        com.songshu.voluntarylabour.pojo.User user = userMapper.selectOne(wrapper);

        if(user == null) {
            throw new UsernameNotFoundException("用户名不存在!");
        }

        List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("role");
        return new User(user.getUsername(),new BCryptPasswordEncoder().encode(user.getPassword()),auths);
    }
}

6、在启动类上添加注解 @MapperScan

@SpringBootApplication
@MapperScan("com.songshu.voluntarylabour.mapper")
public class VoluntarylabourApplication {

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

}

七、自定义登录页面

1、创建相关页面和Controller

自定义登录页面:

<form action="/test/login" method="post">
	用户名:<input type="text" name="username" />
	<br/>
	密码:<input type="password" name="password" />
	<br/>
	<input type="submit" value="登录"/>
</form>

编写Controller类:

@RestController
@RequestMapping("/user")
public class HelloController {

    @GetMapping("say")
    public String say() {
        return "hello security!!!!";
    }

    @GetMapping("index")
    public String login() {
        return "login index!!!";
    }

    @GetMapping("hehe")
    public String hehe() {
        return "hehe!!!";
    }
}

2、在配置类实现相关配置

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    UserDetailsService userDetailsService;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(password());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //自定义自己编写的登录页面
        http.formLogin()
                .loginPage("/login.html") //设置登录页面
                .loginProcessingUrl("/test/login") //登陆访问路径:提交表单之后跳转的地址,可以看作一个中转站,这个步骤就是验证user的一个过程
                .defaultSuccessUrl("/user/index").permitAll() //登录成功后跳转页面路径
                .and().authorizeRequests()
                    .antMatchers("/","/user/say").permitAll() //设置哪些路径可以直接访问,不需要认证
                .anyRequest().authenticated()
                .and().csrf().disable(); //关闭csrf防护
    }

    @Bean
    PasswordEncoder password() {
        return new BCryptPasswordEncoder();
    }
}

八、基于角色或权限进行控制

1、hasAuthority 方法

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

源码:

private static String hasAuthority(String authority) {
	return "hasAuthority('" + authority + "')";
}
1.1、在配置类中当前地址有哪些权限在配置类实现相关配置

在配置类实现相关配置

@Override
protected void configure(HttpSecurity http) throws Exception {
	//自定义自己编写的登录页面
	http.formLogin()
		.loginPage("/login.html") //设置登录页面
		.loginProcessingUrl("/test/login") //登陆访问路径:提交表单之后跳转的地址,可以看作一个中转站,这个步骤就是验证user的一个过程
		.defaultSuccessUrl("/user/index").permitAll() //登录成功后跳转页面路径
		.and().authorizeRequests()
			.antMatchers("/","/user/say").permitAll() //设置哪些路径可以直接访问,不需要认证
			//当前登录路径,只有具有 admin 权限的用户才可以访问这个路基
			.antMatchers("/user/hehe").hasAuthority("admin")
		.anyRequest().authenticated()
		.and().csrf().disable(); //关闭csrf防护
}
1.2、在 MyUserDetailService 类中设置用户权限
@Service("userDetailsService")
public class MyUserDetailService implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        QueryWrapper<com.songshu.voluntarylabour.pojo.User> wrapper = new QueryWrapper();
        wrapper.eq("username",username);
        com.songshu.voluntarylabour.pojo.User user = userMapper.selectOne(wrapper);

        if(user == null) {
            throw new UsernameNotFoundException("用户名不存在!");
        }
		
        //当前用户的权限,实际中可以查数据库设置
        List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admin");
        return new User(user.getUsername(),new BCryptPasswordEncoder().encode(user.getPassword()),auths);
    }
}

2、hasAnyAuthority 方法

源码:

private static String hasAnyAuthority(String... authorities) {
	String anyAuthorities = StringUtils.arrayToDelimitedString(authorities, "','");
	return "hasAnyAuthority('" + anyAuthorities + "')";
}

如果需要设置可以多个权限访问,可以使用 hasAnyAuthority 方法。

如果当前的主体具有指定的权限(设置多个权限),则返回 true ,否则返回 false。

@Override
protected void configure(HttpSecurity http) throws Exception {
	//自定义自己编写的登录页面
	http.formLogin()
		.loginPage("/login.html") //设置登录页面
		.loginProcessingUrl("/test/login") //登陆访问路径:提交表单之后跳转的地址,可以看作一个中转站,这个步骤就是验证user的一个过程
		.defaultSuccessUrl("/user/index").permitAll() //登录成功后跳转页面路径
		.and().authorizeRequests()
			.antMatchers("/","/user/say").permitAll() //设置哪些路径可以直接访问,不需要认证
			//当前登录路径,只有具有 admin 权限的用户才可以访问这个路基
			.antMatchers("/user/hehe").hasAnyAuthority("admin","boss")
		.anyRequest().authenticated()
		.and().csrf().disable(); //关闭csrf防护
}

3、hasRole 方法

源码:

private static String hasRole(String rolePrefix, String role) {
	Assert.notNull(role, "role cannot be null");
	Assert.isTrue(rolePrefix.isEmpty() || !role.startsWith(rolePrefix), () -> {
		return "role should not start with '" + rolePrefix + "' since it is automatically inserted. Got '" + 		role + "'";
	});
	return "hasRole('" + rolePrefix + role + "')";
}

public ExpressionUrlAuthorizationConfigurer(ApplicationContext context) {
	String[] grantedAuthorityDefaultsBeanNames = context.getBeanNamesForType(GrantedAuthorityDefaults.class);
	if (grantedAuthorityDefaultsBeanNames.length == 1) {
		GrantedAuthorityDefaults grantedAuthorityDefaults = (GrantedAuthorityDefaults)context.getBean(grantedAuthorityDefaultsBeanNames[0], GrantedAuthorityDefaults.class);
		this.rolePrefix = grantedAuthorityDefaults.getRolePrefix();
	} else {
		this.rolePrefix = "ROLE_";
	}
	this.REGISTRY = new ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry(context);
}

该方法会给允许访问的页面的权限名称加上一个 ROLE_ 前缀,所以需要在设置用户的权限时注意!

案例:

@Override
    protected void configure(HttpSecurity http) throws Exception {
        //自定义自己编写的登录页面
        http.formLogin()
                .loginPage("/login.html") //设置登录页面
                .loginProcessingUrl("/test/login") //登陆访问路径:提交表单之后跳转的地址,可以看作一个中转站,这个步骤就是验证user的一个过程
                .defaultSuccessUrl("/user/index").permitAll() //登录成功后跳转页面路径
                .and().authorizeRequests()
            		//会默认加上 ROLE_ 前缀,得 ROLE_admin
                    .antMatchers("/user/hehe").hasRole("admin")
                .anyRequest().authenticated()
                .and().csrf().disable(); //关闭csrf防护
    }
@Service("userDetailsService")
public class MyUserDetailService implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        QueryWrapper<com.songshu.voluntarylabour.pojo.User> wrapper = new QueryWrapper();
        wrapper.eq("username",username);
        com.songshu.voluntarylabour.pojo.User user = userMapper.selectOne(wrapper);

        if(user == null) {
            throw new UsernameNotFoundException("用户名不存在!");
        }
		
        //加上前缀
        List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_admin");
        return new User(user.getUsername(),new BCryptPasswordEncoder().encode(user.getPassword()),auths);
    }
}

4、hasAnyRole 方法

针对多个角色。与上面差不多,这里不再演示。

九、自定义403页面

9.1、编写403页面

<body>
    <h1>我是 my403.html 页面,您没有访问权限</h1>
</body>

9.2、配置类中配置

@Override
protected void configure(HttpSecurity http) throws Exception {
	//配置没有权限访问跳转自定义页面
	http.exceptionHandling().accessDeniedPage("/my403.html");
}

十、注解的使用

10.1、@Secured 注解

判断是否具有角色,这里需要注意,匹配的字符串需要加前缀 ”ROLE_ ”。

10.1.1、开启注解功能

在启动类上添加 @EnableGlobalMethodSecurity 注解。

@SpringBootApplication
@MapperScan("com.songshu.voluntarylabour.mapper")
@EnableGlobalMethodSecurity(securedEnabled = true)
public class VoluntarylabourApplication {

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

}
10.1.2、使用 @Secured 注解
@RestController
@RequestMapping("/user")
public class HelloController {
    @GetMapping("eat")
    @Secured({"ROLE_admin","ROLE_boss"})
    public String eat() {
        return "eat eat !!!!";
    }
}

10.2、@PreAuthorize 注解

该注解是用于进入方法前,进行权限认证。@PreAuthorize 可以将登陆用户的 roles / permissions 参数传到方法中。

10.2.1、开启注解功能

在启动类上添加 @EnableGlobalMethodSecurity 注解。

@SpringBootApplication
@MapperScan("com.songshu.voluntarylabour.mapper")
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class VoluntarylabourApplication {

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

}
10.2.2、使用 @PreAuthorize 注解
@RestController
@RequestMapping("/user")
public class HelloController {
    @GetMapping("eat")
//    @Secured({"ROLE_admin","ROLE_boss"})
//    @PreAuthorize("hasRole('admin')")
    @PreAuthorize("hasAnyRole('admin','boss')")
    public String eat() {
        return "eat eat !!!!";
    }
}

10.3、@PostAuthorize 注解

该注解是用于执行完方法后,进行权限认证。与 @PreAuthorize 注解 使用方法一样,但并不常使用,这里边不在演示,感兴趣的可以自行尝试。

10.4、@PreFilter 注解 和 @PostFilter 注解

10.4.1、@PreFilter 注解

对传入方法的数据进行过滤。

10.4.2、@PostFilter注解

对方法返回的数据进行过滤。

十一、用户注销

11.1、在配置类中配置

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    UserDetailsService userDetailsService;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(password());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //配置退出
        http.logout().logoutUrl("/logout").logoutSuccessUrl("/user/logout").permitAll();
        //配置没有权限访问跳转自定义页面
        http.exceptionHandling().accessDeniedPage("/my403.html");
        //自定义自己编写的登录页面
        http.formLogin()
                .loginPage("/login.html") //设置登录页面
                .loginProcessingUrl("/test/login") //登陆访问路径:提交表单之后跳转的地址,可以看作一个中转站,这个步骤就是验证user的一个过程
                .defaultSuccessUrl("/home.html").permitAll() //登录成功后跳转页面路径
                .and().authorizeRequests()
                    .antMatchers("/","/user/say").permitAll() //设置哪些路径可以直接访问,不需要认证
                    .antMatchers("/user/home").hasRole("admin")
                .anyRequest().authenticated()
                .and().csrf().disable(); //关闭csrf防护
    }

    @Bean
    PasswordEncoder password() {
        return new BCryptPasswordEncoder();
    }
}

11.2、编写 home.html 页面和 Contrller

home.html 页面:

<body>
    <h1>登录成功!!</h1>
    <br/>
    <a href="/logout">退出</a>
</body>

编写Controller:

@RestController
@RequestMapping("/user")
public class HelloController {
    @GetMapping("say")
    public String say() {
        return "hello security!!!!";
    }
    
    @GetMapping("home")
    public String home() {
        return "主页主页!!!";
    }
    
    @GetMapping("logout")
    public String logout() {
        return "退出成功!!!";
    }
}

11.2、测试过程

  • 首先访问 localhost:8080/login.html 页面
  • 进行登录,成功后,会跳转到 home.html 页面
  • 点击退出,成功后,访问 /user/logout ,在页面返回 “退出成功!!!”
  • 最后访问 localhost:8080/home ,又需要重新登录

十二、实现自动登录(基于数据库实现记住我)

在 Web 阶段学习,是通过 cookie 技术来实现自动登录。

现在通过安全框架机制来实现自动登录。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-G0hROWM8-1643539747078)(D:\2022面试准备\image-20220130144445511.png)]

12.1、创建表

create table persistent_logins (
	username varchar(64) not null,
	series varchar(64) primary key,
	token varchar(64) not null,
	last_used timestamp not null
)

12.2、在配置类中配置

注入数据源:

@Autowired
private DataSource dataSource;

配置 PersistentTokenRepository 对象:

//配置对象
@Bean
public PersistentTokenRepository persistentTokenRepository() {
	JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
	jdbcTokenRepository.setDataSource(dataSource);
//        jdbcTokenRepository.setCreateTableOnStartup(true);
	return jdbcTokenRepository;
}
@Override
    protected void configure(HttpSecurity http) throws Exception {
        //配置退出
        http.logout().logoutUrl("/logout").logoutSuccessUrl("/user/logout").permitAll();
        //配置没有权限访问跳转自定义页面
        http.exceptionHandling().accessDeniedPage("/my403.html");
        //自定义自己编写的登录页面
        http.formLogin()
                .loginPage("/login.html") //设置登录页面
                .loginProcessingUrl("/test/login") //登陆访问路径:提交表单之后跳转的地址,可以看作一个中转站,这个步骤就是验证user的一个过程
                .defaultSuccessUrl("/home.html").permitAll() //登录成功后跳转页面路径
                .and().authorizeRequests()
                    .antMatchers("/","/user/say").permitAll() //设置哪些路径可以直接访问,不需要认证
                    //当前登录路径,只有具有 admin 权限的用户才可以访问这个路基
//                    .antMatchers("/user/hehe").hasAuthority("role")
//                    .antMatchers("/user/hehe").hasAnyAuthority("admin","boss")
                    .antMatchers("/user/home").hasRole("admin")
                .anyRequest().authenticated()
            	//配置记住我
                .and().rememberMe().tokenRepository(persistentTokenRepository())
                .tokenValiditySeconds(60)   //设置有效时长,以秒为单位
                .userDetailsService(userDetailsService)
                .and().csrf().disable(); //关闭csrf防护
    }