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
@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
配置类中注入自定义加密规则
@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、测试
- 将数据库密码修改为密文
- 成功登陆~
2、带盐值的加密
2.1、概念
借用生活中烹饪时加盐值不同, 菜肴的味道不同这个现象, 在加密时每次使用一个随机生成的盐值, 让加密结果不固定
2.2、测试BCryptPasswordEncoder
- 新建测试类
SecurityTest.java
,测试带盐值的加密
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
长度一定要>=
密码长度)
- 照样能正常访问~~~
3、项目加入SpringSecurity环境
3.1、引入依赖
3.1.1、父工程中统一管理版本
- 在父工程
parent
中,添加统一的依赖版本
<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
的依赖
<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
<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
的配置类
// 表示当前类是一个配置类
@Configuration
// 启用Web环境下权限控制功能
@EnableWebSecurity
public class WebAppSecurityConfig extends WebSecurityConfigurerAdapter {
}
3.4、谁来扫描WebAppSecurityConfig?
- 结论: 为了让
SpringSecurity
能够针对浏览器请求进行权限控制, 需要让SpringMVC
来扫描WebAppSecurityConfig
类。 - 衍生问题:
DelegatingFilterProxy
初始化Filter
时需要一个bean
,这就需要到IOC
容器中去查找,至于去哪个IOC
容器查找,这就要看是谁扫描了WebAppSecurityConfig
。- 如果是
Spring
扫描了WebAppSecurityConfig
, 那么Filter
需要的bean
就在Spring
的IOC
容器。 - 如果是
SpringMVC
扫描了WebAppSecurityConfig
, 那么Filter
需要的bean
就在SpringMVC
的IOC
容器。
- 如果是
3.5、当头一棒
3.5.1、找不到bean
- 启动
web
应用,抛出异常:org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named 'springSecurityFilterChain' available
3.5.2、分析原因
- 明确三大组件启动顺序
- 首先:
ContextLoaderListener
初始化, 创建Spring
的IOC
容器 - 其次:
DelegatingFilterProxy
初始化, 查找IOC
容器、 查找bean
- 最后:
DispatcherServlet
初始化, 创建SpringMVC
的 IOC 容器
- 首先:
DelegatingFilterProxy
查找IOC
容器,然后在IOC
容器中查找bean
的工作机制
3.6、看源码
3.6.1、在Spring ICO容器中查找
- 在
DelegatingFilterProxy
类中的如下代码处打上断点,initFilterBean()
方法负责初始化Filter
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) {
成立
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
容器
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
容器
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
- 其中
attr
为Root WebApplicationContext
,即Spring IOC
容器(父容器)
- 形参
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
容器,就放弃
Step into
进入initDelegate(wac)
:从IOC
容器中获取Filter Bean
protected Filter initDelegate(WebApplicationContext wac) throws ServletException {
Filter delegate = wac.getBean(getTargetBeanName(), Filter.class);
if (isTargetFilterLifecycle()) {
delegate.init(getFilterConfig());
}
return delegate;
}
- 找不到
springSecurityFilterChain
这个Bean
便会抛异常
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
父容器;则在第一次请求时,如果发现Filter
为null
,也会尝试使用IOC
容器去初始化它
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
容器, 找不到, 放弃。- 第一次请求时再次查找。
- 找到
SpringMVC
的IOC
容器。 - 从这个
IOC
容器中找到所需要的bean
。
- 遗憾: 会破坏现有程序的结构。 原本是
ContextLoaderListener
和DispatcherServlet
两个组件创建两个IOC
容器, 现在改成只有一个。
3.8、解决方案二:改源码
3.8.1、思路
-
Web
应用启动时,不让DelegatingFilterProxy
在Spring IOC
容器中去找springSecurityFilterChain
-
第一次请求时,让
DelegatingFilterProxy
在SpringMVC IOC
容器中去找springSecurityFilterChain
3.8.2、改源码
- 在
component
工程下创建与DelegatingFilterProxy
同包名、同类名的类
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
@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
容器的初始化
@Override public void contextInitialized(ServletContextEvent event) { initWebApplicationContext(event.getServletContext()); }
contextInitialized(ServletContextEvent event)
方法中会将IOC
容器放到servletContext
域中
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
- 访问登录页面。又是这熟悉的页面
4、目标一:放行登录页和静态资源
4.1、重写configure方法
- 重写
WebAppSecurityConfig
配置类中的configure
方法
// 表示当前类是一个配置类
@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、实验效果
- 又是这熟悉的首页
5、目标二:登录认证(内存验证)
5.1、修改表单
- 修改登录页面的表单
form
表单的提交地址:action="security/do/login.html"
- 登录异常信息:
${SPRING_SECURITY_LAST_EXCEPTION.message }
<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
会拦截用户的请求- 注释如下代码
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、实验效果
- 又是这熟悉的控制面板
6、目标三:退出登录
6.1、修改请求地址
- 修改顶部导航栏退出登录的请求地址
<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、实验效果
- 乌拉~
7、目标四:登录认证(数据库验证)
7.1、思路
7.2、根据adminId查询管理员信息
7.2.1、Service层
- 在
AdminServiceImpl
中实现此方法
@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层
- 这个方法在之前就已经写好啦~
@Override
public List<Role> getAssignedRole(Integer adminId) {
return roleMapper.selectAssignedRole(adminId);
}
7.3.2、Mapper层
- 来看看之前写的
SQL
语句(子查询)
<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
所拥有的权限
@Override
public List<String> getAssignedAuthNameByAdminId(Integer adminId) {
return authMapper.selectAssignedAuthNameByAdminId(adminId);
}
7.4.2、Mapper层
- 接口方法声明
List<String> selectAssignedAuthNameByAdminId(Integer adminId);
-
编写
SQL
语句t_auth.name
在t_auth
表中
- 将
t_auth
表与inner_role_auth
表左外连接,得到权限与角色的对应关系
- 将
inner_role_auth
与inner_admin_role
表左外连接,得到角色管理员的对应关系
- 最后取出指定
adminId
的权限名称
<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
左外连接分析
7.5、自定义用户类
- 创建
SecurityAdmin
类,继承自User
类,用于封装Admin
的信息
/**
* 考虑到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
对象,并返回此对象
- 装配角色信息(注意添加
@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
方法,使用自定义的登录逻辑
@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、实验测试
- 由于还没有实现加密功能,我们先将数据库密码改为明文密码
Oneby
登陆成功