Security

Author Avatar
kevin
发表:2024-01-26 00:42:00
修改:2024-10-13 20:56:45

Security常用配置

认证和授权的概念

  • 认证:登录(用户名和密码)
    • 登录成功即可认证成功
  • 授权:访问系统功能的权限

权限模块的数据模型

  • 用户表
  • 角色表
    • 必须是ROLE_开头
  • 权限表

security入门

依赖

<dependency>
  <groupId>org.springframework.security</groupId>
  <artifactId>spring-security-web</artifactId>
  <version>5.0.5.RELEASE</version>
</dependency>
<dependency>
  <groupId>org.springframework.security</groupId>
  <artifactId>spring-security-config</artifactId>
  <version>5.0.5.RELEASE</version>
</dependency>

内部封装了Filter,

web.xml容器中配置一个过滤器–代理过滤器,真实的过滤器在spring的容器中配置

配置xml

整合Spring Security时过滤器的名称必须为springSecurityFilterChain

 <filter>
        <!--
			过滤器的名称必须为springSecurityFilterChain,
          否则会抛出NoSuchBeanDefinitionException异常
        -->
        <filter-name>springSecurityFilterChain</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>springSecurityFilterChain</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    <!-- 2:springmvc的核心控制器-->
    <servlet>
        <servlet-name>springmvc</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <!-- 指定加载的配置文件 ,通过参数contextConfigLocation加载 -->
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:spring-security.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>springmvc</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

配置文件

spring-security.xml

<!--
  form-login:定义表单登录信息
            login-page="/login.html":表示指定登录页面
            username-parameter="username":使用登录名的名称,默认值是username
            password-parameter="password":使用登录名的密码,默认值是password
            login-processing-url="/login.do":表示登录的url地址
            default-target-url="/index.html":登录成功后的url地址
            authentication-failure-url="/login.html":认证失败后跳转的url地址,失败后指定/login.html
            always-use-default-target="true":登录成功后,始终跳转到default-target-url指定的地址,即登录成功的默认地址
-->
    <security:http auto-config="true" use-expressions="true">
        <security:intercept-url pattern="/**" access="hasRole('ROLE_ADMIN')"></security:intercept-url>
        <security:form-login login-page="/login.html"
                             username-parameter="username"
                             password-parameter="password"
                             login-processing-url="/login.do"
                             default-target-url="/index.html"
                             authentication-failure-url="/login.html"
                              always-use-default-target="true"
        />
        <!--
  csrf:对应CsrfFilter过滤器
  disabled:是否启用CsrfFilter过滤器,如果使用自定义登录页面需要关闭此项,否则登录操作会被禁用(403)
-->
      <security:csrf disabled="true"></security:csrf>
      </security:http>

对密码进行加密

框架自带加密方式

BCryptPasswordEncoder

在spring-security.xml文件中指定密码加密对象

<!--
    三:认证管理,定义登录账号名和密码,并授予访问的角色、权限
    authentication-manager:认证管理器,用于处理认证操作
-->
<security:authentication-manager>
    <!--
        authentication-provider:认证提供者,执行具体的认证逻辑
    -->
    <security:authentication-provider user-service-ref="userService">
        <!--指定密码加密策略-->
        <security:password-encoder ref="passwordEncoder"></security:password-encoder>
    </security:authentication-provider>
</security:authentication-manager>

<!--配置密码加密对象-->
<bean id="passwordEncoder"
  class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder" />

注解方式权限控制

spring-security.xml文件中配置组件扫描和mvc的注解驱动,用于扫描Controller

开启权限注解支持

Controller的方法上加入注解(@PreAuthorize)进行权限控制

//包扫描
<context:component-scan base-package="com"/>
//注解驱动
<mvc:annotation-driven></mvc:annotation-driven>
<!--开启注解方式权限控制-->
<security:global-method-security pre-post-annotations="enabled" />

创建Controller类并在Controller的方法上加入注解

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

    @RequestMapping("/add")
    @PreAuthorize("hasAuthority('add')")//表示用户必须拥有add权限才能调用当前方法
    public String add(){
        System.out.println("add...");
        return "success";
    }

    @RequestMapping("/update")
    @PreAuthorize("hasRole('ROLE_ADMIN')")//表示用户必须拥有ROLE_ADMIN角色才能调用当前方法
    public String update(){
        System.out.println("update...");
        return "success";
    }

    @RequestMapping("/delete")
    @PreAuthorize("hasRole('ABC')")//表示用户必须拥有ABC角色才能调用当前方法
    public String delete(){
        System.out.println("delete...");
        return "success";
    }
}

退出登录

a连接

<a href="/logout.do">退出登录</a>

spring-security.xml定义:

<!--
  logout:退出登录
  logout-url:退出登录操作对应的请求路径
  logout-success-url:退出登录后的跳转页面
  invalidate-session="true" 默认为true,用户在退出后Http session失效
-->
<security:logout logout-url="/logout.do"
                 logout-success-url="/login.html" invalidate-session="true"/>

总结

不需要登录,权限 角色 就可以访问

<security:http security="none" pattern="/js/**"></security:http>
<security:http security="none" pattern="/css/**"></security:http>
<security:http security="none" pattern="/login.html"></security:http>

使用指定的登录页面

<security:form-login login-page="/login.html"
                     username-parameter="username"
                     password-parameter="password"
                     login-processing-url="/login.do"
                     default-target-url="/index.html"
                     authentication-failure-url="/login.html"
                     always-use-default-target="true"/>

从数据库查询用户信息,对密码进行加密

<security:authentication-manager>
    <security:authentication-provider user-service-ref="userService">
        <security:password-encoder ref="bCryptPasswordEncoder"></security:password-encoder>
    </security:authentication-provider>
</security:authentication-manager>
<bean id="bCryptPasswordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"></bean>

对访问的页面做权限控制

<security:intercept-url pattern="/index.html" access="isAuthenticated()"></security:intercept-url>
<security:intercept-url pattern="/a.html" access="isAuthenticated()"></security:intercept-url>
<security:intercept-url pattern="/b.html" access="hasAuthority('add')"></security:intercept-url>
<security:intercept-url pattern="/c.html" access="hasRole('ROLE_ADMIN')"></security:intercept-url>
<security:intercept-url pattern="/d.html" access="hasRole('ABC')"></security:intercept-url>

注解方式权限控制

<security:global-method-security pre-post-annotations="enabled"></security:global-method-security>

退出登录

<security:logout logout-url="/logout.do" logout-success-url="/login.html" invalidate-session="true"></security:logout>

SpringSecurity-原理

代码必须配置

名字必须叫springSecurityFilterChain

<filter>
        <filter-name>springSecurityFilterChain</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>springSecurityFilterChain</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

当我们使用基于Spring Security的NameSpace进行配置时

系统会自动为我们注册一个名为springSecurityFilterChain类型为FilterChainProxy的bean,所以我们声明的过滤器必须叫springSecurityFilterChain

DelegatingFilterProxy

DelegatingFilterProxy 代理的就是名为 SpringSecurityFilterChain的Filter

本身是和spring security无关,继承于抽象类GenericFilterBean,间接地实现了javax.servlet.Filter接口

FilterChainProxy

获取过滤器链中的过滤器,封装为虚拟的VirtualFilterChain对象,并开始执行过滤

调用链1-2024-10-13-20-53-48.png

public class DelegatingFilterProxy extends GenericFilterBean {
    
    protected void initFilterBean() throws ServletException {
        // 找到 springSecurityFilterChain
        this.targetBeanName = getFilterName();
    }
}

vfc全称 VirtualFilterChain对象

UsernamePasswordAuthenticationFilter

处理 form 登陆的过滤器

① 属性中增加了username和password字段;
② 强制的只对POST请求应用;
③ 重写了attemptAuthentication身份验证入口方法。

public class UsernamePasswordAuthenticationFilter extends
		AbstractAuthenticationProcessingFilter{
    // 判断参数是否是username和password
    public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
	public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
    // 必须是post提交
    private boolean postOnly = true;
    
    public Authentication attemptAuthentication{
        // 获取用户名和密码
         // request 中提取用户名和密码。
        String username = obtainUsername(request);
		String password = obtainPassword(request);

		if (username == null) {
			username = "";
		}

		if (password == null) {
			password = "";
		}

        //用户名去两边空格
		username = username.trim();
        // 封装用户名和密码
          // 封装类UsernamePasswordAuthenticationToken ,用来存储及传递用户名和用户密码。
		UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
				username, password);
        // 获取权限管理器
         //调用getAuthenticationManager()方法获取AuthenticationManager实例
        //authenticate()方法对封装在authRequest [UsernamePasswordAuthenticationToken]中的用户名及密码进行验证。
        return this.getAuthenticationManager().authenticate(authRequest);
    }
    
}

AuthenticationManager这里可以理解为 spring security 配置文件中 <authentication-manager/> 的实现类

执行顺序

1)	用户通过url:/login 登录,该过滤器接收表单用户名密码

2)	判断用户名密码是否为空

3)	生成 UsernamePasswordAuthenticationToken

4)	将 Authentiction 传给 AuthenticationManager接口的 authenticate 方法进行认证处理

5)	AuthenticationManager 默认是实现类为 ProviderManager ,ProviderManager 委托给 AuthenticationProvider 进行处理

6)	UsernamePasswordAuthenticationFilter 继承了 AbstractAuthenticationProcessingFilter 抽象类,AbstractAuthenticationProcessingFilter 在 successfulAuthentication 方法中对登录成功进行了处理,通过 SecurityContextHolder.getContext().setAuthentication() 方法将 Authentication 认证信息对象绑定到 SecurityContext

7)	下次请求时,在过滤器链头的 SecurityContextPersistenceFilter 会从 Session 中取出用户信息并生成 Authentication(默认为 UsernamePasswordAuthenticationToken),并通过 SecurityContextHolder.getContext().setAuthentication() 方法将 Authentication 认证信息对象绑定到 SecurityContext

8)	需要权限才能访问的请求会从 SecurityContext 中获取用户的权限进行验证 

DaoAuthenticationProvider

解析并认证,需要一个提供者,这就是提供者

就是根据 UsernamePasswordAuthenticationToken,获取到 username,然后调用 UserDetailsService 检索用户详细信息

public class DaoAuthenticationProvider{
    protected final UserDetails retrieveUser{  
        UserDetails loadedUser = getUserDetailsService().loadUserByUsername(username);
    }
}

进入到自定义用户名的方法

@Component
public class UserService implements UserDetailsService {
    
     @Override
    public UserDetails loadUserByUsername(String username)  {
        
         // 交给springsecurity进行认证和授权
         return new User(username,password,list);
    }
}

AuthorityUtils

是一个工具包,用于操作 GrantedAuthority 集合的实用方法:

commaSeparatedStringToAuthorityList(String authorityString):从逗号分隔符中创建 GrantedAuthority 对象数组,帮我们快速创建出权限的集合

注解配置

web.xml 配置

添加 SpringSecurityFilter 进行安全控制

<filter>
    <!--名称固定,不能变-->
    <filter-name>springSecurityFilterChain</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
    <filter-name>springSecurityFilterChain</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>
  <!--设置核心控制器-->
    <servlet>
        <servlet-name>dispatcherServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:spring.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>dispatcherServlet</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

spring配置 : spring.xml,使用的是默认的视图解析器

 <!--包扫描-->
    <context:component-scan base-package="com.kevin" />

    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/views/"></property>
        <property name="suffix" value=".html"></property>
    </bean>
    <mvc:annotation-driven />
    <mvc:default-servlet-handler />

新建 IndexController

@Controller
public class IndexController {
    @GetMapping("index")
    @ResponseBody
    public String index(){
        return "success";
    }
}

设置配置类

@Configuration
@EnableWebSecurity
public class AppWebSecurityConfig extends WebSecurityConfigurerAdapter {

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

    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()// 自定义自己编写的登录页面
                .loginPage("/login.html") // 配置哪个 url 为登录页面
                .loginProcessingUrl("/login") // 设置哪个是登录的 url。
                .defaultSuccessUrl("/views/success.html") // 登录成功之后跳转到哪个 url
                .failureForwardUrl("/views/failure.html") // 登录失败之后跳转到哪个 url
                .permitAll()

        .and().authorizeRequests()
                .antMatchers("/views/index.html").permitAll()  //设置哪些路径可以直接访问,不需要认证

                .anyRequest() // 其他请求
                .authenticated() //需要认证
// 关闭 csrf
 //退出连接             			     .logout().logoutUrl("/logout").logoutSuccessUrl("/login.html").permitAll()
        .and().csrf().disable();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //加密策略
        auth.userDetailsService(userDetailsService).passwordEncoder(password());
    }
}

基于角色或权限进行访问控制

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

调用链2-2024-10-13-20-54-38.png

hasAnyAuthority 方法,有任何提供的角色,返回true

调用链3-2024-10-13-20-55-29.png

hasRole 方法

如果用户具备给定角色就允许访问,否则出现 403。
如果当前主体具有指定的角色,则返回 true。

配置文件中不需要添加”ROLE_“,因为底层代码会自动添加与之进行匹配

调用链4-2024-10-13-20-56-02.png

hasAnyRole方法, 表示用户具备任何一个条件都可以访问。

调用链5-2024-10-13-20-56-38.png

自定义编写实现类

@Service("userDetailsService")
public class LoginService implements UserDetailsService {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        String queryUser = "SELECT * FROM `users` WHERE username=?";

//1、查询指定用户的信息
        Map<String, Object> map = jdbcTemplate.queryForMap(queryUser, username);

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

//2、将查询到的用户封装到框架使用的UserDetails里面
        return new User(map.get("username").toString(), map.get("password").toString(),
                AuthorityUtils.createAuthorityList("ADMIN","USER"));//暂时写死,过后数据库中查
    }

}

开启注解功能!

@Configuration
@EnableWebSecurity
//开启注解功能!
@EnableGlobalMethodSecurity(securedEnabled=true,prePostEnabled = true)
public class AppWebSecurityConfig extends WebSecurityConfigurerAdapter {
} 

// 测试注解:

@Secured这里匹配的字符串需要添加前缀“ROLE_“。

@Controller
public class IndexController {

    // 测试注解:
    @RequestMapping("testSecured")
    @ResponseBody
   // @Secured({"ROLE_normal","ROLE_admin"})
    //@PreAuthorize("hasRole('ROLE_管理员')")
    @PreAuthorize("hasAnyAuthority('admins')")
    public String helloUser() {
        return "hello,user";
    }
}    

注意事项

页面提交方式必须为 post 请求,用户名,密码必须为

username,password

页面添加记住我复选框

<form action="/login" method="post">
    username:<input type="text" name="username"><br>
    password:<input type="password" name="password"><br>
    记住我:<input type="checkbox"name="remember-me"title="记住密码"/><br/>
    <input type="submit" value="submit">
</form>

name 属性值必须位 remember-me.不能改为其他值

    @Autowired
    private DataSource dataSource;
    @Bean
    public PersistentTokenRepository persistentTokenRepository(){
        JdbcTokenRepositoryImpl jdbcTokenRepository = new
                JdbcTokenRepositoryImpl();
// 赋值数据源
        jdbcTokenRepository.setDataSource(dataSource);
// 自动创建表,第一次执行会创建,以后要执行就要删除掉!
//        jdbcTokenRepository.setCreateTableOnStartup(true);
        return jdbcTokenRepository;
    }

实现记住我

 .and()
 //实现记住我
            .rememberMe()
            //多久时间免登录,
            .tokenValiditySeconds(7200)// 单位秒
            //往数据库缓存
            .tokenRepository(persistentTokenRepository())
            //自动登录
            .userDetailsService(userDetailsService)
评论