day08【后台】权限控制-上

day08【后台】权限控制-上

1、密码加密

1.1、PasswordEncoder接口

  • PasswordEncoder接口的代码如下:
    • 将明文密码加密为密文密码
    • 判断明文密码是否与密文密码一致
public interface PasswordEncoder {

	/**
	 * Encode the raw password. Generally, a good encoding algorithm applies a SHA-1 or
	 * greater hash combined with an 8-byte or greater randomly generated salt.
	 */
	String encode(CharSequence rawPassword);

	/**
	 * Verify the encoded password obtained from storage matches the submitted raw
	 * password after it too is encoded. Returns true if the passwords match, false if
	 * they do not. The stored password itself is never decoded.
	 *
	 * @param rawPassword the raw password to encode and match
	 * @param encodedPassword the encoded password from storage to compare with
	 * @return true if the raw password, after encoding, matches the encoded password from
	 * storage
	 */
	boolean matches(CharSequence rawPassword, String encodedPassword);

}

1.2、自定义加密规则

  • 自定义加密规则:MyPasswordEncoder.java

image-20200617210026056

@Component
public class MyPasswordEncoder implements PasswordEncoder {

	@Override
	public String encode(CharSequence rawPassword) {
		
		return privateEncode(rawPassword);
		
	}

	@Override
	public boolean matches(CharSequence rawPassword, String encodedPassword) {
		
		// 1.对明文密码进行加密
		String formPassword = privateEncode(rawPassword);
		
		// 2.声明数据库密码
		String databasePassword = encodedPassword;
		
		// 3.比较
		return Objects.equals(formPassword, databasePassword);
	}
	
	private String privateEncode(CharSequence rawPassword) {
		try {
			// 1.创建MessageDigest对象
			String algorithm = "MD5";
			MessageDigest messageDigest = MessageDigest.getInstance(algorithm);
			
			// 2.获取rawPassword的字节数组
			byte[] input = ((String)rawPassword).getBytes();
			
			// 3.加密
			byte[] output = messageDigest.digest(input);
			
			// 4.转换为16进制数对应的字符
			String encoded = new BigInteger(1, output).toString(16).toUpperCase();
			
			return encoded;
			
		} catch (NoSuchAlgorithmException e) {
			e.printStackTrace();
			return null;
		}
	}

}

1.3、注入自定义加密规则

  • WebAppSecurityConfig.java配置类中注入自定义加密规则

image-20200617210929062

@Autowired
private MyPasswordEncoder myPasswordEncoder;

@Override
protected void configure(AuthenticationManagerBuilder builder) throws Exception {
    /*
		builder
			.inMemoryAuthentication()			// 在内存中完成账号、密码的检查
			.withUser("tom")					// 指定账号
			.password("123123")					// 指定密码
			.roles("ADMIN","学徒")				// 指定当前用户的角色
			.and()
			.withUser("jerry")					// 指定账号
			.password("123123")					// 指定密码
			.authorities("UPDATE","内门弟子")		// 指定当前用户的权限
			;
		*/

    // 装配userDetailsService对象
    builder
        .userDetailsService(myUserDetailsService)
        .passwordEncoder(myPasswordEncoder)
        ;

}

1.4、测试

  • 将数据库密码修改为密文

image-20200617211145976

  • 成功登陆~

image-20200617211250409

2、带盐值的加密

2.1、概念

借用生活中烹饪时加盐值不同, 菜肴的味道不同这个现象, 在加密时每次使用一个随机生成的盐值, 让加密结果不固定

image-20200919195949522

2.2、测试BCryptPasswordEncoder

  • 新建测试类SecurityTest.java,测试带盐值的加密

image-20200617222634183

public class SecurityTest {
	
	public static void main(String[] args) {
		
		// 1.创建BCryptPasswordEncoder对象
		BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
		
		// 2.准备明文字符串
		String rawPassword = "123123";
		
		// 3.加密
		String encode = passwordEncoder.encode(rawPassword);
		System.out.println(encode);
		// $2a$10$3YODojJmtbcOzHqB6bjZhO2CR7l9pPDfxBsnYz2voBHw5Ro.5bMAm
		// $2a$10$UeZXBF9bPMipZuFp0djjj.vnShI2J097JtgGHAZX5mtJ49FMiP6XK
		// $2a$10$ucdma3RFSGVgNc30nGxu9Oku66A4zHNbaxTgxRq4ucpbw7ZmeK.8m
		// $2a$10$kpGqUaCvRGA5rE0.GNyvGugHBPZPM8bKQ96KJl9vxyx/N3bif72OS
		// $2a$10$MSDMkdzhHGIj8o6A7RUa7Oe3Iww13jy7IQRCEG.aOJa6/uI2U/Pem
	}

}

class EncodeTest {
	
	public static void main(String[] args) {
		
		// 1.准备明文字符串
		String rawPassword = "123123";
		
		// 2.准备密文字符串
		String encodedPassword = "$2a$10$ucdma3RFSGVgNc30nGxu9Oku66A4zHNbaxTgxRq4ucpbw7ZmeK.8m";
		
		// 3.创建BCryptPasswordEncoder对象
		BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
		
		// 4.比较
		boolean matcheResult = passwordEncoder.matches(rawPassword, encodedPassword);
		
		// 一致
		System.out.println(matcheResult ? "一致" : "不一致");
	}
	
}

2.3、带盐值的验证

  • 重写configure方法:使用BCryptPasswordEncoder进行加密
// 每次调用这个方法时会检查IOC容器中是否有了对应的bean,如果有就不会真正执行这个函数,因为bean默认是单例的
// 也可以使用@Scope(value="")注解控制是否单例
@Bean
public BCryptPasswordEncoder getBCryptPasswordEncoder() {
    return new BCryptPasswordEncoder();
}

@Override
protected void configure(AuthenticationManagerBuilder builder) throws Exception {
    /*
		builder
			.inMemoryAuthentication()			// 在内存中完成账号、密码的检查
			.withUser("tom")					// 指定账号
			.password("123123")					// 指定密码
			.roles("ADMIN","学徒")				// 指定当前用户的角色
			.and()
			.withUser("jerry")					// 指定账号
			.password("123123")					// 指定密码
			.authorities("UPDATE","内门弟子")		// 指定当前用户的权限
			;
		*/

    // 装配userDetailsService对象
    builder
        .userDetailsService(myUserDetailsService)
        .passwordEncoder(getBCryptPasswordEncoder())
        ;

}
  • 修改Heygo的密码(注意:如果userpswd长度一定要 >= 密码长度)

image-20200617223435942

  • 照样能正常访问~~~

image-20200617224041799

3、项目加入SpringSecurity环境

3.1、引入依赖

3.1.1、父工程中统一管理版本
  • 在父工程parent中,添加统一的依赖版本

image-20200617225512695

<properties>
    <!-- 声明属性, 对 Spring 的版本进行统一管理 -->
    <atguigu.spring.version>4.3.20.RELEASE</atguigu.spring.version>
    <!-- 声明属性, 对 SpringSecurity 的版本进行统一管理 -->
    <atguigu.spring.security.version>4.2.10.RELEASE</atguigu.spring.security.version>
</properties>

<dependencies>
    
    <!-- 此处省略若干依赖... -->
    
    <!-- SpringSecurity 对 Web 应用进行权限管理 -->
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-web</artifactId>
        <version>${atguigu.spring.security.version}</version>
    </dependency>
    <!-- SpringSecurity 配置 -->
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-config</artifactId>
        <version>${atguigu.spring.security.version}</version>
    </dependency>
    <!-- SpringSecurity 标签库 -->
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-taglibs</artifactId>
        <version>${atguigu.spring.security.version}</version>
    </dependency>
</dependencies>
3.1.2、子工程中引入依赖
  • component工程中引入SpringSecurity的依赖

image-20200617225852001

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-web</artifactId>
</dependency>
<!-- SpringSecurity 配置 -->
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-config</artifactId>
</dependency>
<!-- SpringSecurity 标签库 -->
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-taglibs</artifactId>
</dependency>

3.2、配置DelegatingFilterProxy

  • web.xml中配置DelegatingFilterProxy

image-20200617225938179

<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>

3.3、创建基注解的配置类

  • component工程下创建SpringSecurity的配置类

image-20200617230150789

// 表示当前类是一个配置类
@Configuration

// 启用Web环境下权限控制功能
@EnableWebSecurity
public class WebAppSecurityConfig extends WebSecurityConfigurerAdapter {
	
}

3.4、谁来扫描WebAppSecurityConfig?

  • 结论: 为了让 SpringSecurity 能够针对浏览器请求进行权限控制, 需要让SpringMVC 来扫描 WebAppSecurityConfig 类。
  • 衍生问题: DelegatingFilterProxy 初始化Filter时需要一个 bean,这就需要到 IOC 容器中去查找,至于去哪个IOC容器查找,这就要看是谁扫描了 WebAppSecurityConfig
    • 如果是 Spring 扫描了 WebAppSecurityConfig, 那么 Filter 需要的 bean 就在SpringIOC 容器。
    • 如果是 SpringMVC 扫描了 WebAppSecurityConfig, 那么 Filter 需要的 bean就在 SpringMVCIOC 容器。

3.5、当头一棒

3.5.1、找不到bean
  • 启动web应用,抛出异常:org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named 'springSecurityFilterChain' available

image-20200618104939591

3.5.2、分析原因
  • 明确三大组件启动顺序
    • 首先: ContextLoaderListener 初始化, 创建 SpringIOC 容器
    • 其次: DelegatingFilterProxy 初始化, 查找 IOC 容器、 查找 bean
    • 最后: DispatcherServlet 初始化, 创建 SpringMVC 的 IOC 容器
  • DelegatingFilterProxy 查找 IOC 容器,然后在 IOC 容器中查找 bean 的工作机制

image-20200618105700854

3.6、看源码

3.6.1、在Spring ICO容器中查找
  • DelegatingFilterProxy类中的如下代码处打上断点,initFilterBean()方法负责初始化Filter

image-20200618110828514

private volatile Filter delegate;

@Override
protected void initFilterBean() throws ServletException {
    synchronized (this.delegateMonitor) {
        if (this.delegate == null) {
            // If no target bean name specified, use filter name.
            if (this.targetBeanName == null) {
                this.targetBeanName = getFilterName();
            }
            // Fetch Spring root application context and initialize the delegate early,
            // if possible. If the root application context will be started after this
            // filter proxy, we'll have to resort to lazy initialization.
            WebApplicationContext wac = findWebApplicationContext();
            if (wac != null) {
                this.delegate = initDelegate(wac);
            }
        }
    }
}
  • Step into进入findWebApplicationContext()方法:寻找IOC容器
    • if (this.webApplicationContext != null) {成立
    • if (attrName != null) {成立

image-20200618111022162

protected WebApplicationContext findWebApplicationContext() {
    if (this.webApplicationContext != null) {
        // The user has injected a context at construction time -> use it...
        if (this.webApplicationContext instanceof ConfigurableApplicationContext) {
            ConfigurableApplicationContext cac = (ConfigurableApplicationContext) this.webApplicationContext;
            if (!cac.isActive()) {
                // The context has not yet been refreshed -> do so before returning it...
                cac.refresh();
            }
        }
        return this.webApplicationContext;
    }
    String attrName = getContextAttribute();
    if (attrName != null) {
        return WebApplicationContextUtils.getWebApplicationContext(getServletContext(), attrName);
    }
    else {
        return WebApplicationContextUtils.findWebApplicationContext(getServletContext());
    }
}
  • Step into进入WebApplicationContextUtils.findWebApplicationContext(getServletContext())方法:寻找IOC容器

image-20200618111440279

public static WebApplicationContext findWebApplicationContext(ServletContext sc) {
    WebApplicationContext wac = getWebApplicationContext(sc);
    if (wac == null) {
        Enumeration<String> attrNames = sc.getAttributeNames();
        while (attrNames.hasMoreElements()) {
            String attrName = attrNames.nextElement();
            Object attrValue = sc.getAttribute(attrName);
            if (attrValue instanceof WebApplicationContext) {
                if (wac != null) {
                    throw new IllegalStateException("No unique WebApplicationContext found: more than one " +
                                                    "DispatcherServlet registered with publishContext=true?");
                }
                wac = (WebApplicationContext) attrValue;
            }
        }
    }
    return wac;
}
  • Step into进入getWebApplicationContext(sc)方法中:从ServletContext中获取IOC容器

image-20200618111502379

String ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE = WebApplicationContext.class.getName() + ".ROOT";

public static WebApplicationContext getWebApplicationContext(ServletContext sc) {
    return getWebApplicationContext(sc, WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
}
  • Step into进入getWebApplicationContext(sc, WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE)方法中:获取整个Web应用的父容器
    • 形参attrName = org.springframework.web.context.WebApplicationContext.ROOT
    • 其中attrRoot WebApplicationContext,即Spring IOC容器(父容器)

image-20200618111913427

public static WebApplicationContext getWebApplicationContext(ServletContext sc, String attrName) {
    Assert.notNull(sc, "ServletContext must not be null");
    Object attr = sc.getAttribute(attrName);
    if (attr == null) {
        return null;
    }
    if (attr instanceof RuntimeException) {
        throw (RuntimeException) attr;
    }
    if (attr instanceof Error) {
        throw (Error) attr;
    }
    if (attr instanceof Exception) {
        throw new IllegalStateException((Exception) attr);
    }
    if (!(attr instanceof WebApplicationContext)) {
        throw new IllegalStateException("Context attribute is not of type WebApplicationContext: " + attr);
    }
    return (WebApplicationContext) attr;
}
  • 得到IOC容器之后,就会使用IOC容器中的Bean初始化Filter;如果找不到IOC容器,就放弃

image-20200618112545068

  • Step into进入initDelegate(wac):从IOC容器中获取Filter Bean

image-20200618113057953

protected Filter initDelegate(WebApplicationContext wac) throws ServletException {
    Filter delegate = wac.getBean(getTargetBeanName(), Filter.class);
    if (isTargetFilterLifecycle()) {
        delegate.init(getFilterConfig());
    }
    return delegate;
}
  • 找不到springSecurityFilterChain这个Bean便会抛异常

image-20200618113323441

public boolean filterStart() {

    if (getLogger().isDebugEnabled()) {
        getLogger().debug("Starting filters");
    }
    // Instantiate and record a FilterConfig for each defined filter
    boolean ok = true;
    synchronized (filterConfigs) {
        filterConfigs.clear();
        for (Entry<String,FilterDef> entry : filterDefs.entrySet()) {
            String name = entry.getKey();
            if (getLogger().isDebugEnabled()) {
                getLogger().debug(" Starting filter '" + name + "'");
            }
            try {
                ApplicationFilterConfig filterConfig =
                    new ApplicationFilterConfig(this, entry.getValue());
                filterConfigs.put(name, filterConfig);
            } catch (Throwable t) {
                t = ExceptionUtils.unwrapInvocationTargetException(t);
                ExceptionUtils.handleThrowable(t);
                getLogger().error(sm.getString(
                    "standardContext.filterStart", name), t);
                ok = false;
            }
        }
    }

    return ok;
}
  • 如果初始化Filter时,没有找到Spring IOC父容器;则在第一次请求时,如果发现Filternull,也会尝试使用IOC容器去初始化它

image-20200618113844354

public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
    throws ServletException, IOException {

    // Lazily initialize the delegate if necessary.
    Filter delegateToUse = this.delegate;
    if (delegateToUse == null) {
        synchronized (this.delegateMonitor) {
            delegateToUse = this.delegate;
            if (delegateToUse == null) {
                WebApplicationContext wac = findWebApplicationContext();
                if (wac == null) {
                    throw new IllegalStateException("No WebApplicationContext found: " +
                                                    "no ContextLoaderListener or DispatcherServlet registered?");
                }
                delegateToUse = initDelegate(wac);
            }
            this.delegate = delegateToUse;
        }
    }

    // Let the delegate perform the actual doFilter operation.
    invokeDelegate(delegateToUse, request, response, filterChain);
}
  • 结论:Spring IOC容器为整个Web应用的父容器,然后在Spring IOC容器中并没有springSecurityFilterChain这个Bean,所以程序便会抛异常

3.7、解决方案一: IOC容器合二为一

  • 不使用 ContextLoaderListener, 让 DispatcherServlet 加载所有 Spring 配置文件。
    • DelegatingFilterProxy 在初始化时查找 IOC 容器, 找不到, 放弃。
    • 第一次请求时再次查找。
    • 找到 SpringMVCIOC 容器。
    • 从这个 IOC 容器中找到所需要的 bean
  • 遗憾: 会破坏现有程序的结构。 原本是 ContextLoaderListenerDispatcherServlet 两个组件创建两个 IOC 容器, 现在改成只有一个。

3.8、解决方案二:改源码

3.8.1、思路
  • Web应用启动时,不让DelegatingFilterProxySpring IOC容器中去找springSecurityFilterChain

  • 第一次请求时,让DelegatingFilterProxySpringMVC IOC容器中去找springSecurityFilterChain

3.8.2、改源码
  • component工程下创建与DelegatingFilterProxy同包名、同类名的类

image-20200618140809664

  • Web应用启动时不加载Spring IOC容器
@Override
protected void initFilterBean() throws ServletException {
    synchronized (this.delegateMonitor) {
        if (this.delegate == null) {
            // If no target bean name specified, use filter name.
            if (this.targetBeanName == null) {
                this.targetBeanName = getFilterName();
            }
            // Fetch Spring root application context and initialize the delegate early,
            // if possible. If the root application context will be started after this
            // filter proxy, we'll have to resort to lazy initialization.
            /*
				WebApplicationContext wac = findWebApplicationContext();
				if (wac != null) {
					this.delegate = initDelegate(wac);
				}*/
        }
    }
}
  • 第一次请求时,去SpringMVC IOC容器中获取springSecurityFilterChain这个Bean

image-20200618141236041

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
    throws ServletException, IOException {

    // Lazily initialize the delegate if necessary.
    Filter delegateToUse = this.delegate;
    if (delegateToUse == null) {
        synchronized (this.delegateMonitor) {
            delegateToUse = this.delegate;
            if (delegateToUse == null) {

                // 把原来的查找IOC容器的代码注释掉
                // WebApplicationContext wac = findWebApplicationContext();

                // 按我们自己的需要重新编写
                // 1.获取ServletContext对象
                ServletContext sc = this.getServletContext();

                // 2.拼接SpringMVC将IOC容器存入ServletContext域的时候使用的属性名
                String servletName = "springDispatcherServlet";

                String attrName = FrameworkServlet.SERVLET_CONTEXT_PREFIX + servletName;

                // 3.根据attrName从ServletContext域中获取IOC容器对象
                WebApplicationContext wac = (WebApplicationContext) sc.getAttribute(attrName);

                if (wac == null) {
                    throw new IllegalStateException("No WebApplicationContext found: " +
                                                    "no ContextLoaderListener or DispatcherServlet registered?");
                }
                delegateToUse = initDelegate(wac);
            }
            this.delegate = delegateToUse;
        }
    }

    // Let the delegate perform the actual doFilter operation.
    invokeDelegate(delegateToUse, request, response, filterChain);
}
3.8.3、为什么可以这样改?
  • IOC容器初始化好了之后就会被放在ServletContext

    • ContextLoaderListener中的contextInitialized(ServletContextEvent event)方法中会执行IOC容器的初始化

    image-20200618141528845

    @Override
    public void contextInitialized(ServletContextEvent event) {
        initWebApplicationContext(event.getServletContext());
    }
    
    • contextInitialized(ServletContextEvent event)方法中会将IOC容器放到servletContext域中

    image-20200618141909245

    public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
        if (servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE) != null) {
            throw new IllegalStateException(
                "Cannot initialize context because there is already a root application context present - " +
                "check whether you have multiple ContextLoader* definitions in your web.xml!");
        }
    
        Log logger = LogFactory.getLog(ContextLoader.class);
        servletContext.log("Initializing Spring root WebApplicationContext");
        if (logger.isInfoEnabled()) {
            logger.info("Root WebApplicationContext: initialization started");
        }
        long startTime = System.currentTimeMillis();
    
        try {
            // Store context in local instance variable, to guarantee that
            // it is available on ServletContext shutdown.
            if (this.context == null) {
                this.context = createWebApplicationContext(servletContext);
            }
            if (this.context instanceof ConfigurableWebApplicationContext) {
                ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context;
                if (!cwac.isActive()) {
                    // The context has not yet been refreshed -> provide services such as
                    // setting the parent context, setting the application context id, etc
                    if (cwac.getParent() == null) {
                        // The context instance was injected without an explicit parent ->
                        // determine parent for root web application context, if any.
                        ApplicationContext parent = loadParentContext(servletContext);
                        cwac.setParent(parent);
                    }
                    configureAndRefreshWebApplicationContext(cwac, servletContext);
                }
            }
            servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
    
            ClassLoader ccl = Thread.currentThread().getContextClassLoader();
            if (ccl == ContextLoader.class.getClassLoader()) {
                currentContext = this.context;
            }
            else if (ccl != null) {
                currentContextPerThread.put(ccl, this.context);
            }
    
            if (logger.isDebugEnabled()) {
                logger.debug("Published root WebApplicationContext as ServletContext attribute with name [" +
                             WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE + "]");
            }
            if (logger.isInfoEnabled()) {
                long elapsedTime = System.currentTimeMillis() - startTime;
                logger.info("Root WebApplicationContext: initialization completed in " + elapsedTime + " ms");
            }
    
            return this.context;
        }
        catch (RuntimeException ex) {
            logger.error("Context initialization failed", ex);
            servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, ex);
            throw ex;
        }
        catch (Error err) {
            logger.error("Context initialization failed", err);
            servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, err);
            throw err;
        }
    }
    

3.9、实验效果

  • 启动Web应用并没有抛异常,并且我们还发现了 SpringSecurity 的工作原理: 在初始化时或第一次请求时准备好过滤器链。具体任务由具体过滤器来完成。
org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter
org.springframework.security.web.context.SecurityContextPersistenceFilter
org.springframework.security.web.header.HeaderWriterFilter
org.springframework.security.web.csrf.CsrfFilter
org.springframework.security.web.authentication.logout.LogoutFilter
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter
org.springframework.security.web.authentication.www.BasicAuthenticationFilter
org.springframework.security.web.savedrequest.RequestCacheAwareFilter
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter
org.springframework.security.web.authentication.AnonymousAuthenticationFilter
org.springframework.security.web.session.SessionManagementFilter
org.springframework.security.web.access.ExceptionTranslationFilter
org.springframework.security.web.access.intercept.FilterSecurityInterceptor
  • 访问登录页面。又是这熟悉的页面

image-20200618142832128

4、目标一:放行登录页和静态资源

4.1、重写configure方法

  • 重写WebAppSecurityConfig配置类中的configure方法

image-20200617230150789

// 表示当前类是一个配置类
@Configuration

// 启用Web环境下权限控制功能
@EnableWebSecurity
public class WebAppSecurityConfig extends WebSecurityConfigurerAdapter {
	
	@Override
	protected void configure(HttpSecurity security) throws Exception {
		
		security
			.authorizeRequests()						// 对请求进行授权
			.antMatchers("/admin/to/login/page.html")	// 针对登录页进行设置
			.permitAll()								// 无条件访问
			
			// 放行静态资源
			.antMatchers("/bootstrap/**")	
			.permitAll()                    
			.antMatchers("/crowd/**")       
			.permitAll()                    
			.antMatchers("/css/**")         
			.permitAll()                    
			.antMatchers("/fonts/**")       
			.permitAll()                    
			.antMatchers("/img/**")         
			.permitAll()                    
			.antMatchers("/jquery/**")      
			.permitAll()                    
			.antMatchers("/layer/**")       
			.permitAll()                    
			.antMatchers("/script/**")      
			.permitAll()                    
			.antMatchers("/ztree/**")       
			.permitAll()
			.anyRequest()					// 其他任意请求
			.authenticated()				// 认证后访问
			;
		
	}
	
}

4.2、实验效果

  • 又是这熟悉的首页

image-20200618143840613

5、目标二:登录认证(内存验证)

5.1、修改表单

  • 修改登录页面的表单
    • form表单的提交地址:action="security/do/login.html"
    • 登录异常信息:${SPRING_SECURITY_LAST_EXCEPTION.message }

image-20200618144502366

<form action="security/do/login.html" method="post" class="form-signin" role="form">
	<h2 class="form-signin-heading">
		<i class="glyphicon glyphicon-log-in"></i> 管理员登录
	</h2>
	<p>${requestScope.exception.message }</p>
	<p>${SPRING_SECURITY_LAST_EXCEPTION.message }</p>
	<div class="form-group has-success has-feedback">
		<input type="text" name="loginAcct" value="Heygo" class="form-control" id="inputSuccess4"
			placeholder="请输入登录账号" autofocus> <span
			class="glyphicon glyphicon-user form-control-feedback"></span>
	</div>
	<div class="form-group has-success has-feedback">
		<input type="text" name="userPswd" value="123123" class="form-control" id="inputSuccess4"
			placeholder="请输入登录密码" style="margin-top: 10px;"> <span
			class="glyphicon glyphicon-lock form-control-feedback"></span>
	</div>
	<button type="submit" class="btn btn-lg btn-success btn-block">登录</button>
</form>

5.2、重写configure方法

  • 重写配置类中的configure方法
    • 指定登录页面地址
    • 指定提交登录请求的地址
    • 指定登陆成功后要去的页面
    • 指定用户名和密码的字段名称
    • 配置内存版的tom用户
// 表示当前类是一个配置类
@Configuration

// 启用Web环境下权限控制功能
@EnableWebSecurity
public class WebAppSecurityConfig extends WebSecurityConfigurerAdapter {
	
	@Override
	protected void configure(HttpSecurity security) throws Exception {
		
		security
			.authorizeRequests()						// 对请求进行授权
			.antMatchers("/admin/to/login/page.html")	// 针对登录页进行设置
			.permitAll()								// 无条件访问
			
			// 放行静态资源
			.antMatchers("/bootstrap/**")	
			.permitAll()                    
			.antMatchers("/crowd/**")       
			.permitAll()                    
			.antMatchers("/css/**")         
			.permitAll()                    
			.antMatchers("/fonts/**")       
			.permitAll()                    
			.antMatchers("/img/**")         
			.permitAll()                    
			.antMatchers("/jquery/**")      
			.permitAll()                    
			.antMatchers("/layer/**")       
			.permitAll()                    
			.antMatchers("/script/**")      
			.permitAll()                    
			.antMatchers("/ztree/**")       
			.permitAll()
			.anyRequest()					// 其他任意请求
			.authenticated()				// 认证后访问
			.and()
			.csrf()							// 防跨站请求伪造功能
			.disable()						// 禁用
			.formLogin()					// 开启表单登录的功能
			.loginPage("/admin/to/login/page.html")			// 指定登录页面
			.loginProcessingUrl("/security/do/login.html")	// 指定处理登录请求的地址
			.defaultSuccessUrl("/admin/to/main/page.html")	// 指定登录成功后前往的地址
			.usernameParameter("loginAcct")	// 账号的请求参数名称
			.passwordParameter("userPswd")	// 密码的请求参数名称
			;
		
	}
	
	@Override
	protected void configure(AuthenticationManagerBuilder builder) throws Exception {
		
		// 临时使用内存版登录的模式测试代码
		builder
			.inMemoryAuthentication()
			.withUser("Heygo")
			.password("123123")
			.roles("ADMIN");
		
	}
	
}

5.3、干掉拦截器

  • SpringMVC配置文件中干掉Interceptor,因为Interceptor会拦截用户的请求

    • 注释如下代码

    image-20200618152126681

    image-20200618152351884

    • Interceptor代码如下
    public class LoginInterceptor extends HandlerInterceptorAdapter {
    
    	@Override
    	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
    			throws Exception {
    		// 1.通过request对象获取Session对象
    		HttpSession session = request.getSession();
    		
    		// 2.尝试从Session域中获取Admin对象
    		Admin admin = (Admin) session.getAttribute(CrowdConstant.ATTR_NAME_LOGIN_ADMIN);
    
    		// 3.判断admin对象是否为空
    		if (admin == null) {
    			// 4.抛出异常
    			throw new AccessForbiddenException(CrowdConstant.MESSAGE_ACCESS_FORBIDEN);
    		}
    
    		// 5.如果Admin对象不为null,则返回true放行
    		return true;
    	}
    
    }
    

5.4、实验效果

  • 又是这熟悉的控制面板

image-20200618152556010

6、目标三:退出登录

6.1、修改请求地址

  • 修改顶部导航栏退出登录的请求地址

image-20200618153123799

<li><a href="seucrity/do/logout.html"><i
	class="glyphicon glyphicon-off"></i> 退出系统</a></li>

6.2、重写configure方法

  • 重写configure方法:
    • 开启退出登录功能
    • 制定退出登录的请求地址
    • 指定退出登录成功后。需要重定向至哪个页面
@Override
protected void configure(HttpSecurity security) throws Exception {

    security
        .authorizeRequests()						// 对请求进行授权
        .antMatchers("/admin/to/login/page.html")	// 针对登录页进行设置
        .permitAll()								// 无条件访问

        // 放行静态资源
        .antMatchers("/bootstrap/**")	
        .permitAll()                    
        .antMatchers("/crowd/**")       
        .permitAll()                    
        .antMatchers("/css/**")         
        .permitAll()                    
        .antMatchers("/fonts/**")       
        .permitAll()                    
        .antMatchers("/img/**")         
        .permitAll()                    
        .antMatchers("/jquery/**")      
        .permitAll()                    
        .antMatchers("/layer/**")       
        .permitAll()                    
        .antMatchers("/script/**")      
        .permitAll()                    
        .antMatchers("/ztree/**")       
        .permitAll()
        .anyRequest()					// 其他任意请求
        .authenticated()				// 认证后访问
        .and()
        .csrf()							// 防跨站请求伪造功能
        .disable()						// 禁用
        .formLogin()					// 开启表单登录的功能
        .loginPage("/admin/to/login/page.html")			// 指定登录页面
        .loginProcessingUrl("/security/do/login.html")	// 指定处理登录请求的地址
        .defaultSuccessUrl("/admin/to/main/page.html")	// 指定登录成功后前往的地址
        .usernameParameter("loginAcct")	// 账号的请求参数名称
        .passwordParameter("userPswd")	// 密码的请求参数名称
        .and()
        .logout()						// 开启退出登录功能
        .logoutUrl("/seucrity/do/logout.html")			// 指定退出登录地址
        .logoutSuccessUrl("/admin/to/login/page.html")	// 指定退出成功以后前往的地址
        ;

}

6.3、实验效果

  • 乌拉~

image-20200618153329287

7、目标四:登录认证(数据库验证)

7.1、思路

image-20200919200415416

7.2、根据adminId查询管理员信息

7.2.1、Service层
  • AdminServiceImpl中实现此方法

image-20200618191410829

@Override
public Admin getAdminByLoginAcct(String username) {
    // 查询条件
    AdminExample example = new AdminExample();
    Criteria criteria = example.createCriteria();
    criteria.andLoginAcctEqualTo(username);

    // 执行查询
    List<Admin> list = adminMapper.selectByExample(example);
    Admin admin = list.get(0);

    //返回Admin对象
    return admin;
}
7.2.2、Mapper层
  • Mapper层代码由Mybatis逆向生成

7.3、根据adminId查询管理员所拥有的角色

7.3.1、Service层
  • 这个方法在之前就已经写好啦~

image-20200618160220166

@Override
public List<Role> getAssignedRole(Integer adminId) {
    return roleMapper.selectAssignedRole(adminId);
}
7.3.2、Mapper层
  • 来看看之前写的SQL语句(子查询)

image-20200618160350468

<select id="selectAssignedRole" resultMap="BaseResultMap">
    select id,name from t_role where id in (select role_id from inner_admin_role where admin_id=#{adminId})
</select>

7.4、根据adminId查询管理员所拥有的权限名

7.4.1、Service层
  • 根据adminId的值查询该Admin所拥有的权限

image-20200618160540655

@Override
public List<String> getAssignedAuthNameByAdminId(Integer adminId) {
    return authMapper.selectAssignedAuthNameByAdminId(adminId);
}

7.4.2、Mapper层
  • 接口方法声明

image-20200618160646021

List<String> selectAssignedAuthNameByAdminId(Integer adminId);
  • 编写SQL语句

    • t_auth.namet_auth表中

    image-20200618161313485

    • t_auth表与inner_role_auth表左外连接,得到权限与角色的对应关系

    image-20200618161345541

    • inner_role_authinner_admin_role表左外连接,得到角色管理员的对应关系

    image-20200618161257172

    • 最后取出指定adminId的权限名称

image-20200618160705011

<select id="selectAssignedAuthNameByAdminId" resultType="string">
    SELECT DISTINCT t_auth.name 
    FROM t_auth 
    LEFT JOIN inner_role_auth ON t_auth.id=inner_role_auth.auth_id
    LEFT JOIN inner_admin_role ON inner_admin_role.role_id=inner_role_auth.role_id
    WHERE inner_admin_role.admin_id=#{adminId} and t_auth.name != "" and t_auth.name is not null
</select>
  • SQL左外连接分析

image-20200618161923159

7.5、自定义用户类

  • 创建SecurityAdmin类,继承自User类,用于封装Admin的信息

image-20200618190423315

/**
 * 考虑到User对象中仅仅包含账号和密码,为了能够获取到原始的Admin对象,专门创建这个类对User类进行扩展
 * @author Lenovo
 *
 */
public class SecurityAdmin extends User {
	
	private static final long serialVersionUID = 1L;
	
	// 原始的Admin对象,包含Admin对象的全部属性
	private Admin originalAdmin;
	
	public SecurityAdmin(
			// 传入原始的Admin对象
			Admin originalAdmin, 
			
			// 创建角色、权限信息的集合
			List<GrantedAuthority> authorities) {
		
		// 调用父类构造器
		super(originalAdmin.getLoginAcct(), originalAdmin.getUserPswd(), authorities);
		
		// 给本类的this.originalAdmin赋值
		this.originalAdmin = originalAdmin;
		
	}
	
	// 对外提供的获取原始Admin对象的getXxx()方法
	public Admin getOriginalAdmin() {
		return originalAdmin;
	}

}

7.6、实现UserDetailsService接口

  • component工程下创建UserDetailsService接口的实现类:CrowdUserDetailsService
    • 装配角色信息(注意添加"ROLE_"前缀)
    • 装配权限信息
    • 封装成securityAdmin对象,并返回此对象

image-20200618192921708

@Component
public class CrowdUserDetailsService implements UserDetailsService {
	
	@Autowired
	private AdminService adminService;
	
	@Autowired
	private RoleService roleService;
	
	@Autowired
	private AuthService authService;

	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		
		// 1.根据账号名称查询Admin对象
		Admin admin = adminService.getAdminByLoginAcct(username);
		
		// 2.获取adminId
		Integer adminId = admin.getId();
		
		// 3.根据adminId查询角色信息
		List<Role> assignedRoleList = roleService.getAssignedRole(adminId);
		
		// 4.根据adminId查询权限信息
		List<String> authNameList = authService.getAssignedAuthNameByAdminId(adminId);
		
		// 5.创建集合对象用来存储GrantedAuthority
		List<GrantedAuthority> authorities = new ArrayList<>();
		
		// 6.遍历assignedRoleList存入角色信息
		for (Role role : assignedRoleList) {
			
			// 注意:不要忘了加前缀!
			String roleName = "ROLE_" + role.getName();
			
			SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(roleName);
			
			authorities.add(simpleGrantedAuthority);
		}
		
		// 7.遍历authNameList存入权限信息
		for (String authName : authNameList) {
			
			SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(authName);
			
			authorities.add(simpleGrantedAuthority);
		}
		
		// 8.封装SecurityAdmin对象
		SecurityAdmin securityAdmin = new SecurityAdmin(admin, authorities);
		
		return securityAdmin;
	}

}

7.7、注入UserDetailsService

  • 在配置类中重写configure方法,使用自定义的登录逻辑

image-20200618192900194

@Autowired
private UserDetailsService userDetailsService;

@Override
protected void configure(AuthenticationManagerBuilder builder) throws Exception {

    // 临时使用内存版登录的模式测试代码
    /*
		builder
			.inMemoryAuthentication()
			.withUser("Heygo")
			.password("123123")
			.roles("ADMIN");
		*/

    builder.userDetailsService(userDetailsService);

}

7.8、实验测试

  • 由于还没有实现加密功能,我们先将数据库密码改为明文密码

image-20200618193110679

  • Oneby登陆成功

image-20200618193308066

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
  个人博客系统(HituxBlog)是专业为个人建站而开发的一款网站程序。该系统采用最流行的ASP+ACCESS进行搭建,页面使用DIV+CSS进行编写,全面兼容时下IE、FireFox、Chrome等主流浏览器。系统内置多达30款主题及精美相册,后台一键切换。前台所有内容均可以在后台进行修改删除等操作。   通过该系统建立您的博客或者是个人网站将变得轻而易举。不需要具备多么专业的网页设计知识,不需要对程序有多熟悉,仅仅下载海纳个人博客的源码上传到您申请的空间里,即生成了您的网站。接下来您要做的只是对网站的更新,写一篇文章,或是上传一张图片。将更多的精力用在宣传您的网站上,而不是建立网站。21世纪人人上网,人人有网站的时代,您不再无助,HituxBlog愿助您一臂之力,携手共进! 系统无与伦比的五大特色: 1、页面设计够简单,拒绝花俏; 2、便捷后台,管理前台所有内容; 3、页面全静态化,易优化且高效; 4、内置多达30款主题,自由切换; 5、相册展示模式,美伦美奂。 -------------------------安 全 建 议------------------------------ 后台管理地址:http://你的网站域名/AdminCool/login.asp 用户名:admin 密码:admin 后台文件夹名:AdminCool 数据库存放位置:DatabaseX 提醒:为确保网站安全,我们建议: 1、更改默认的后台用户名和密码
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值