# SpringSecruityNotes **Repository Path**: Sunxz007/SpringSecruityNotes ## Basic Information - **Project Name**: SpringSecruityNotes - **Description**: SpringSecruity 存储笔记中的示例源码 - **Primary Language**: Java - **License**: Artistic-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2020-06-09 - **Last Updated**: 2020-12-19 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # SpringSecruityNotes #### 介绍 SpringSecruity 存储笔记中的示例源码 #### 目录 - [环境搭建说明](#%E7%8E%AF%E5%A2%83%E6%90%AD%E5%BB%BA%E8%AF%B4%E6%98%8E) - [基本环境引入](#%E5%9F%BA%E6%9C%AC%E7%8E%AF%E5%A2%83%E5%BC%95%E5%85%A5) - [引入Spring Secruity](#%E5%BC%95%E5%85%A5spring-secruity) - [静态资源的配置](#%E9%9D%99%E6%80%81%E8%B5%84%E6%BA%90%E7%9A%84%E9%85%8D%E7%BD%AE) - [SpringSecurity 的默认配置](#springsecurity-%E7%9A%84%E9%BB%98%E8%AE%A4%E9%85%8D%E7%BD%AE) - [放行首页和静态资源](#%E6%94%BE%E8%A1%8C%E9%A6%96%E9%A1%B5%E5%92%8C%E9%9D%99%E6%80%81%E8%B5%84%E6%BA%90) - [未认证请求跳转到登录页](#%E6%9C%AA%E8%AE%A4%E8%AF%81%E8%AF%B7%E6%B1%82%E8%B7%B3%E8%BD%AC%E5%88%B0%E7%99%BB%E5%BD%95%E9%A1%B5) - [loginPage(String loginPage) 方法的说明](#loginpagestring-loginpage-%E6%96%B9%E6%B3%95%E7%9A%84%E8%AF%B4%E6%98%8E) - [loginProcessingUrl()方法指定登录地址](#loginprocessingurl%E6%96%B9%E6%B3%95%E6%8C%87%E5%AE%9A%E7%99%BB%E5%BD%95%E5%9C%B0%E5%9D%80) - [设置登录系统的账号、密码并登录](#%E8%AE%BE%E7%BD%AE%E7%99%BB%E5%BD%95%E7%B3%BB%E7%BB%9F%E7%9A%84%E8%B4%A6%E5%8F%B7%E5%AF%86%E7%A0%81%E5%B9%B6%E7%99%BB%E5%BD%95) - [设置登录系统的账号密码](#%E8%AE%BE%E7%BD%AE%E7%99%BB%E5%BD%95%E7%B3%BB%E7%BB%9F%E7%9A%84%E8%B4%A6%E5%8F%B7%E5%AF%86%E7%A0%81) - [设置账号和密码的参数名,指定请求成功后的地址](#%E8%AE%BE%E7%BD%AE%E8%B4%A6%E5%8F%B7%E5%92%8C%E5%AF%86%E7%A0%81%E7%9A%84%E5%8F%82%E6%95%B0%E5%90%8D%E6%8C%87%E5%AE%9A%E8%AF%B7%E6%B1%82%E6%88%90%E5%8A%9F%E5%90%8E%E7%9A%84%E5%9C%B0%E5%9D%80) - [**用户注销**](#%E7%94%A8%E6%88%B7%E6%B3%A8%E9%94%80) - [基于角色或权限进行访问控制](#%E5%9F%BA%E4%BA%8E%E8%A7%92%E8%89%B2%E6%88%96%E6%9D%83%E9%99%90%E8%BF%9B%E8%A1%8C%E8%AE%BF%E9%97%AE%E6%8E%A7%E5%88%B6) - [配置角色权限](#%E9%85%8D%E7%BD%AE%E8%A7%92%E8%89%B2%E6%9D%83%E9%99%90) - [自定义 403 错误页面](#%E8%87%AA%E5%AE%9A%E4%B9%89-403-%E9%94%99%E8%AF%AF%E9%A1%B5%E9%9D%A2) - [记住我(不重要)](#%E8%AE%B0%E4%BD%8F%E6%88%91%E4%B8%8D%E9%87%8D%E8%A6%81) - [内存版](#%E5%86%85%E5%AD%98%E7%89%88) - [记住我原理简要分析](#%E8%AE%B0%E4%BD%8F%E6%88%91%E5%8E%9F%E7%90%86%E7%AE%80%E8%A6%81%E5%88%86%E6%9E%90) - [数据库版本](#%E6%95%B0%E6%8D%AE%E5%BA%93%E7%89%88%E6%9C%AC) - [**查询数据库完成认证**](#%E6%9F%A5%E8%AF%A2%E6%95%B0%E6%8D%AE%E5%BA%93%E5%AE%8C%E6%88%90%E8%AE%A4%E8%AF%81) - [SpringSecurity 默认实现](#springsecurity-%E9%BB%98%E8%AE%A4%E5%AE%9E%E7%8E%B0) - [自定义数据库查询方式](#%E8%87%AA%E5%AE%9A%E4%B9%89%E6%95%B0%E6%8D%AE%E5%BA%93%E6%9F%A5%E8%AF%A2%E6%96%B9%E5%BC%8F) - [**应用自定义密码加密规则**](#%E5%BA%94%E7%94%A8%E8%87%AA%E5%AE%9A%E4%B9%89%E5%AF%86%E7%A0%81%E5%8A%A0%E5%AF%86%E8%A7%84%E5%88%99) - [盐值加密](#%E7%9B%90%E5%80%BC%E5%8A%A0%E5%AF%86) ## 环境搭建说明 ### 基本环境引入 引入springmvc的环境和添加测试使用的资源,这个步骤可以直接引入tag0.0.1 就布置好了基本环境 此时可以填入登录名密码来随意访问演示项目中的任意资源 ![截屏2020-06-09下午5.32.24](https://gitee.com/Sunxz007/PicStorage/raw/master/imgs/20200609173233.png) ### 引入Spring Secruity 1. spring环境统一配置为 5.2.5.RELEASE,加入 SpringMVC 环境需要的依赖 ```xml org.springframework.security spring-security-web ${spring.version} org.springframework.security spring-security-config ${spring.version} org.springframework.security spring-security-taglibs ${spring.version} ``` 2. 在 web.xml 中加入 SpringSecurity 控制权限的 Filter > SpringSecurity使用的是过滤器Filter而不是拦截器Interceptor,因此能够管理的不仅仅是 SpringMVC 中的 handler 请求,还包含 Web 应用中所有请求。比如: 项目中的静态资源也会被拦截,从而进行权限控制。 ```xml springSecurityFilterChain org.springframework.web.filter.DelegatingFilterProxy springSecurityFilterChain /* ``` **注意** `springSecurityFilterChain` 标签中必须是 springSecurityFilterChain。因为 springSecurityFilterChain 在 IOC 容器中对应真正执行权限控制的二十几个 Filter,只有叫这个名字才能够加载到这些 Filter。 3. 加入配置类 com.sun.security.config.WebAppSecurityConfig ```java @Configuration @EnableWebSecurity public class WebAppSecurityConfig extends WebSecurityConfigurerAdapter { } ``` 使用springsecruity接管系统来测试 此时打开项目请求会转发到login界面中,静态资源也都被拦截,要求登录。 ![截屏2020-06-09下午5.30.59](https://gitee.com/Sunxz007/PicStorage/raw/master/imgs/20200609173134.png) ## 静态资源的配置 ### SpringSecurity 的默认配置 在自定义配置类`WebAppSecurityConfig` 继承的父类的configure 方法中可以看到spring secruity 的默认配置,默认会把所有请求转发 到form表单请求页 ```java protected void configure(HttpSecurity http) throws Exception { logger.debug("Using default configure(HttpSecurity). If subclassed this will potentially override subclass configure(HttpSecurity)."); http .authorizeRequests() .anyRequest().authenticated() .and() .formLogin().and() .httpBasic(); } ``` 此时所有请求都会被重定向到Spring Security 默认的登录页 ![截屏2020-06-09下午5.30.59](https://gitee.com/Sunxz007/PicStorage/raw/master/imgs/20200609173134.png) ### 放行首页和静态资源 可以通过重写此方法来自定义放行配置 ```java @Override protected void configure(HttpSecurity security) throws Exception { //super.configure(security); 注释掉将取消父类方法中的默认规则 security.authorizeRequests() //对请求进行授权 .antMatchers("/layui/**","/index.jsp") //使用 ANT 风格设置要授权的 URL 地 址 .permitAll() //允许上面使用 ANT 风格设置的全部请求 .anyRequest() //其他未设置的全部请求 .authenticated(); //需要认证 } ``` 此时layui目录下的静态资源可以访问,并且项目首页也能正常显示 ![截屏2020-06-10下午2.34.22](https://gitee.com/Sunxz007/PicStorage/raw/master/imgs/20200610143452.png) 但是剩余的资源没有配置放行条件,一律被拦截 ![截屏2020-06-10下午2.35.39](https://gitee.com/Sunxz007/PicStorage/raw/master/imgs/20200610143602.png) ## 未认证请求跳转到登录页 继续修改configure 方法 ```java @Override protected void configure(HttpSecurity security) throws Exception { //super.configure(security); 注释掉将取消父类方法中的默认规则 security.authorizeRequests() //对请求进行授权 .antMatchers("/layui/**", "/index.jsp") //使用 ANT 风格设置要授权的 URL 地 址 .permitAll() //允许上面使用 ANT 风格设置的全部请求 .anyRequest() //其他未设置的全部请求 .authenticated(); //需要认证 .and() .formLogin() //设置未授权请求跳转到登录页面 .loginPage("/index.jsp") //指定登录页 .permitAll(); //为登录页设置所有人都可以访问 } ``` 此时再请求未授权的资源(例如:`http://localhost:8080/SpringSecruityNotes_war/main.html`)就会重定向到`项目地址/index.jsp`界面 ![截屏2020-06-10下午3.05.51](https://gitee.com/Sunxz007/PicStorage/raw/master/imgs/20200610150626.png) ### loginPage(String loginPage) 方法的说明 更新loginPage的值会影响许多其他默认值,`登录表单地址`,`退出登录地址`,`登录失败地址` 例如 以下是设定formLogin()时的默认值 >/login GET - the login form >/login POST - process the credentials and if valid authenticate the user >/login?error GET - redirect here for failed authentication attempts >/login?logout GET - redirect here after successfully logging out 如果将"/authenticate"传递给此方法,则将更新如下所示的缺省值 >/authenticate GET - 登录表单 >/authenticate POST - 登录后处理凭证,如果有效就验证用户 >/authenticate?error GET -身份验证尝试失败时,此处重定向错误 >/authenticate?logout GET - 成功登出后在这里重定向 于是登录的表单地址就会修改为为自定义的登录界面 ### loginProcessingUrl()方法指定登录地址 loginProcessingUrl()方法指定了登录地址,就会覆盖 loginPage()方法中设置的默认值 ## 设置登录系统的账号、密码并登录 验证的流程图 ![截屏2020-06-10下午3.12.31](https://gitee.com/Sunxz007/PicStorage/raw/master/imgs/20200610151340.png) [commit:**放行静态资源和首页,无权限资源跳转自定义登录页面配置**](https://gitee.com/Sunxz007/SpringSecruityNotes/commit/e1e4a5a94a549f9adc98d4dd90a06b2e0febe5e6) ### 设置登录系统的账号密码 重写另外一个父类方法`configure(AuthenticationManagerBuilder builder) ` ```java /** * 配置为无需加密,此方法在5.0以上已弃用,要求必须有默认的PasswordEncoder * 这里只做演示需要暂用 */ @Bean public static NoOpPasswordEncoder passwordEncoder() { return (NoOpPasswordEncoder) NoOpPasswordEncoder.getInstance(); } @Override protected void configure(AuthenticationManagerBuilder builder) throws Exception { //super.configure(auth); 一定要禁用默认规则 builder.inMemoryAuthentication() .withUser("tom") .password("123123") //设置账号密码 .roles("ADMIN") //设置角色 .and() .withUser("jerry") .password("456456") //设置另一个账号密码 .authorities("SAVE", "EDIT"); //设置权限 } ``` Cannot pass a null GrantedAuthority collection 问题是由于没设置 roles() 或 authorities()方法导致的。 更改登录页面的表单请求 ### 设置账号和密码的参数名,指定请求成功后的地址 默认账号、密码的请求参数名 >SpringSecurity 默认账号的请求参数名:username > >SpringSecurity 默认密码的请求参数名:password 自定义请求参数名可以修改页面上的表单项的 name 属性值,并在配置中指定 ```html //.... ``` **注意:**input的name属性值必须符合SpringSecurity规则,除非专门进行了定制,否则用户名必须使用username,密码必须使用password 使用usernameParameter()和 passwordParameter()方法修改请求参数配置,同时指定登录成功后的跳转界面 ```java @Override protected void configure(HttpSecurity security) throws Exception { //super.configure(security); 注释掉将取消父类方法中的默认规则 security.authorizeRequests() //对请求进行授权 .antMatchers("/layui/**", "/index.jsp") //使用 ANT 风格设置要授权的 URL 地 址 .permitAll() //允许上面使用 ANT 风格设置的全部请求 .anyRequest() .authenticated() .and() .formLogin() //设置未授权请求跳转到登录页面 .loginPage("/index.jsp") //指定登录页 .loginProcessingUrl("/do/login.html") .permitAll() //登录地址本身也需要放行 .usernameParameter("loginAcct") // 定制登录账号的请求参数名 .passwordParameter("userPswd") // 定制登录密码的请求参数名 .defaultSuccessUrl("/main.html"); //设置登录成功后默认前往的 URL 地址// 指定提交登录表单的地址 } ``` 最后取消原来的登录请求,给界面设置form表单,指向登录请求地址 ```html

${SPRING_SECURITY_LAST_EXCEPTION.message}

……
``` **_csrf** 为防止跨站请求伪造 > 发送登录请求时没有携带_csrf 值,则返回下面错误: > > ![截屏2020-06-10下午4.20.46](https://gitee.com/Sunxz007/PicStorage/raw/master/imgs/20200610162210.png) > > 从钓鱼网站的页面提交的请求无法携带正确、被承认的令牌 > > ![截屏2020-06-10下午4.23.29](https://gitee.com/Sunxz007/PicStorage/raw/master/imgs/20200610162359.png) 此时登录能够实现 ![截屏2020-06-10下午4.24.48](https://gitee.com/Sunxz007/PicStorage/raw/master/imgs/20200610162627.png) [commit:**设置登录系统的账号、密码并登录获取请求的资源**](https://gitee.com/Sunxz007/SpringSecruityNotes/commit/477b72ac9cf693c6b0b6e50661ff7423761f5c43) ## **用户注销** 通过调用 HttpSecurity 对象的一系列方法设置注销功能。 * logout()方法:开启注销功能 * logoutUrl()方法:自定义注销功能的 URL 地址 * logoutSuccessUrl()方法:退出成功后前往的 URL 地址 * addLogoutHandler()方法:添加退出处理器 * logoutSuccessHandler()方法:退出成功处理器 **注意:**如果 CSRF 功能没有禁用,那么退出请求必须是 POST 方式。如果禁用了 CSRF 功能则任何请求方式都可以。 ```java @Override protected void configure(HttpSecurity security) throws Exception { //super.configure(security); 注释掉将取消父类方法中的默认规则 security.authorizeRequests() //对请求进行授权 .antMatchers("/layui/**", "/index.jsp") //使用 ANT 风格设置要授权的 URL 地 址 .permitAll() //允许上面使用 ANT 风格设置的全部请求 .anyRequest() .authenticated() .and() .formLogin() //设置未授权请求跳转到登录页面 .loginPage("/index.jsp") //指定登录页 .loginProcessingUrl("/do/login.html") .usernameParameter("loginAcct") // 定制登录账号的请求参数名 .passwordParameter("userPswd") // 定制登录密码的请求参数名 .defaultSuccessUrl("/main.html") //设置登录成功后默认前往的 URL 地址 .and() //.csrf() //.disable() //禁用csfr .logout() .logoutUrl("/do/logout.html") .logoutSuccessUrl("/") ; } ``` [commit:**用户注销**](https://gitee.com/Sunxz007/SpringSecruityNotes/commit/8210e5544dada06a746a8cc7a4947a04d3121638) ## 基于角色或权限进行访问控制 ### 配置角色权限 通过 HttpSecurity 对象设置资源的角色要求 ```java @Override protected void configure(HttpSecurity security) throws Exception { //super.configure(security); 注释掉将取消父类方法中的默认规则 security.authorizeRequests() //对请求进行授权 .antMatchers("/layui/**", "/index.jsp") //使用 ANT 风格设置要授权的 URL 地 址 .permitAll() //允许上面使用 ANT 风格设置的全部请求 .antMatchers("/level1/**") .hasRole("学徒") .antMatchers("/level2/**") .hasRole("大师") .antMatchers("/level3/**") .hasRole("宗师") .anyRequest() .authenticated() .... ``` 通过 AuthenticationManagerBuilder 对象设置用户登录时具备的角色 ```java @Override protected void configure(AuthenticationManagerBuilder builder) throws Exception { //super.configure(auth); 一定要禁用默认规则 builder.inMemoryAuthentication() .withUser("tom") .password("123123") //设置账号密码 .roles("ADMIN","学徒","宗师") //设置角色 .and() .withUser("jerry") .password("456456") //设置另一个账号密码 .authorities("SAVE", "EDIT"); //设置权限 } ``` 成功登录有权限的页面 ![截屏2020-06-10下午5.03.57](https://gitee.com/Sunxz007/PicStorage/raw/master/imgs/20200610170604.png) 未分配角色的无权限登录 ![截屏2020-06-10下午5.04.12](https://gitee.com/Sunxz007/PicStorage/raw/master/imgs/20200610170612.png) **注意:** 1. 调用顺序的范围从小到大,如果先调用了为设置请求需要验证,则权限匹配会被覆盖 ```java .antMatchers("/level1/**") //设置匹配/level1/**的地址 .hasRole("学徒") //要求具备“学徒”角色 .antMatchers("/level2/**") .hasRole("大师") .antMatchers("/level3/**") .hasRole("宗师") .anyRequest() //其它未设置的所有请求 .authenticated() //需要认证才可以访问 ``` 2. SpringSecurity 会在角色字符串前面加“ROLE\_”前缀。权限不会加,但是从数据库查询得到的用户信息、角色信息、权限信息需要我们自己手动组装。手动组装时需要我们自己给角色字符串前面加“ROLE\_”前缀。 ![截屏2020-06-10下午5.19.34](https://gitee.com/Sunxz007/PicStorage/raw/master/imgs/20200610173039.png) 3. 验证时会验证是否以Role_开头,所以角色名不能为 ROLE\_ 开头 ![截屏2020-06-10下午5.25.41](https://gitee.com/Sunxz007/PicStorage/raw/master/imgs/20200610173028.png) [commit:**配置角色访问权限**](https://gitee.com/Sunxz007/SpringSecruityNotes/commit/140209a86a44fc5ad99d2ae255cb683347449e6e) ### 自定义 403 错误页面 * 可以使用SpringSecurity提供的默认处理器处理,转发到默认界面 在config中继续配置exceptionHandling ```java .and() .exceptionHandling() //指定异常处理器 .accessDeniedPage("/to/no/auth/page.html") //访问被拒绝时前往的页面 ``` 然后编写contrller方法处理请求转发返回的具体页面 ```java @RequestMapping("/to/no/auth/page.html") public String toNoAuthPage() { //请求返回的地址 return "no_auth"; } ``` * 也可以通过`accessDeniedHandler`方法,自定错误处理的方法,此方法需传入一个实现了 AccessDeniedHandler 接口的实现类 ```java .and() .exceptionHandling() //指定异常处理器 //.accessDeniedPage("/to/no/auth/page.html") //访问被拒绝时前往的页面 .accessDeniedHandler(new AccessDeniedHandler() { //自定义错误处理方法 @Override public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException { httpServletRequest.setAttribute("message","抱歉没有访问资源的权限"); httpServletRequest.getRequestDispatcher("/WEB-INF/views/no_auth.jsp").forward(httpServletRequest,httpServletResponse); } }) ``` 此时如没有访问的权限会被默认转发到“no_auth.jsp” ![截屏2020-06-10下午5.48.38](https://gitee.com/Sunxz007/PicStorage/raw/master/imgs/20200610175001.png) [commit:**处理403 错误界面**](https://gitee.com/Sunxz007/SpringSecruityNotes/commit/884c2627858cdc67115f2cedbd1d6e609a96d005) ## 记住我(不重要) ### 内存版 原理 HttpSecurity 对象调用 rememberMe()方法。 ```java .and() .rememberMe() ``` 登录表单携带名为 remember-me 的请求参数。具体做法是将登录表单中的 checkbox 的 name 设置为 `remember-me ` ```java ``` 如果不能使用 “remember-me” 作为请求参数名称,可以使用 `rememberMeParameter()`方法定制。 [commit:**remember-me 使用cookie存储开启**](https://gitee.com/Sunxz007/SpringSecruityNotes/commit/bf1f9e06d5d8cdc2c7246c35a37f15af8c3e65e2) ### 记住我原理简要分析 通过开发者工具看到浏览器端存储了名为remember-me的Cookie。根据这个 Cookie 的 value 在服务器端找到以前登录的 User。而且这个 Cookie 被设置为存储 2 个星期的时间。 ![截屏2020-06-11下午2.00.34](https://gitee.com/Sunxz007/PicStorage/raw/master/imgs/20200611140207.png) ![截屏2020-06-11下午2.02.43](https://gitee.com/Sunxz007/PicStorage/raw/master/imgs/20200611140350.png) ### 数据库版本 服务器重启后,cookie信息可能会清除,为了让服务器重启也不影响记住登录状态,将用户登录状态信息存入数据库。 **使用druid 演示** 引入ORM依赖 ```xml com.alibaba druid 1.1.12 mysql mysql-connector-java 8.0.20 org.springframework spring-orm ${spring.version} ``` 创建数据库 ```sql CREATE DATABASE `securityDemo` CHARACTER SET utf8; ``` 配置数据源 ```xml ``` 在 WebAppSecurityConfig 类中注入数据源 ```java @Autowired private DataSource dataSource; ``` 启用令牌仓库功能 ```java JdbcTokenRepositoryImpl repository = new JdbcTokenRepositoryImpl(); repository.setDataSource(dataSource); //.... HttpSecurity 对象.tokenRepository(repository); ``` 注意:需要进入 JdbcTokenRepositoryImpl 类中找到创建 persistent_logins 表的 SQL 语句创建 initDao() 是protect 的方法,因此调用必须继承JdbcTokenRepositoryImpl ,为简化代码,直接复制其 创建表的sql ![截屏2020-06-11下午2.29.53](https://gitee.com/Sunxz007/PicStorage/raw/master/imgs/20200611143217.png) 创建 persistent_logins 表。 ```sql CREATE TABLE persistent_logins ( username VARCHAR ( 64 ) NOT NULL, series VARCHAR ( 64 ) PRIMARY KEY, token VARCHAR ( 64 ) NOT NULL, last_used TIMESTAMP NOT NULL ); ``` 登录后信息会记录到数据库中 ![截屏2020-06-11下午2.36.27](https://gitee.com/Sunxz007/PicStorage/raw/master/imgs/20200611143826.png) [commit:**remember-me 使用数据库存储token信息**](https://gitee.com/Sunxz007/SpringSecruityNotes/commit/d53d6ca9796afff194538aec5a41ce3179e00021) ## **查询数据库完成认证** ### SpringSecurity 默认实现 builder.jdbcAuthentication().usersByUsernameQuery("tom"); 在 `usersByUsernameQuery("tom")` 等方法中最终调用JdbcDaoImpl类的方法查询 数据库。![截屏2020-06-11下午2.44.28](https://gitee.com/Sunxz007/PicStorage/raw/master/imgs/20200611144516.png) SpringSecurity 的默认实现已经将 SQL 语句硬编码在了 JdbcDaoImpl 类中。这种情况下,我们有下面三种选择: * 按照 JdbcDaoImpl 类中 SQL 语句设计表结构。 * 修改 JdbcDaoImpl 类的源码。 * 不使用 jdbcAuthentication()。 ### 自定义数据库查询方式 实现自定义的数据库查询方式需要自定义实现UserDetailsService接口的类并自动装配,然后使用`builder.userDetailsService(userDetailsService)`加载进springSecurity系统中 新建数据表 ```SQL CREATE TABLE `t_admin` ( `id` int NOT NULL AUTO_INCREMENT, `username` varchar(100) NOT NULL, `userpswd` varchar(100) NOT NULL, `loginacct` varchar(100) NOT NULL, `email` varchar(100) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; INSERT INTO `securityDemo`.`t_admin`(`username`, `userpswd`, `loginacct`, `email`) VALUES ('汤姆', '123456', 'tom', '123@sun.com') ``` 新建Admin bean ```java package com.sun.springsecruity.bean; public class Admin { private Integer id; private String loginacct; private String username; private String userpswd; private String email; // ...getter and setter ``` 实现 UserDetailsService接口的类,并注入spring容器中 ```java @Component public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private JdbcTemplate jdbcTemplate; /** * 根据表单提交的用户名查询User对象,并装配角色权限等信息 * * @param username 表单提交的用户名 * @return userDetails * @throws UsernameNotFoundException 查询错误 */ @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // 1. 根据用户名在数据库中查询 String sql ="select id,loginacct,userpswd,username ,email from t_admin where loginacct= ?"; List list=jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(Admin.class), username); if (list.size()==0) { throw new UsernameNotFoundException("无法查找到用户" + username); } Admin admin = list.get(0); // 2. 给admin设置角色信息 List authorities = new ArrayList<>(); // 添加角色 authorities.add(new SimpleGrantedAuthority("Role_ADMIN")); // 添加权限 authorities.add(new SimpleGrantedAuthority("UPDATE")); // 3. 把admin对象和authorities封装到userDetials 中 return new User(username, admin.getUserpswd(), authorities); } } ``` **注意** 在自定义的 UserDetailsService 中 ,使用`org.springframework.security.core.authority.AuthorityUtils.createAuthorityList(String...) `工具方法获取创建 `SimpleGrantedAuthority` 对象添加角色时需要手动在角色名称前加“ROLE_”前缀。 **使用自定义** **UserDetailsService** **完成登录** ```java // 注入UserDetailsServiceImpl @Autowired private UserDetailsServiceImpl detailsService; @Override protected void configure(AuthenticationManagerBuilder builder) throws Exception { builder.userDetailsService(detailsService); } ``` [commit:**自定义从数据库查询用户完成认证**](https://gitee.com/Sunxz007/SpringSecruityNotes/commit/020f2d3511c0ba39a23287c85d91c89654213d6b) ## **应用自定义密码加密规则** 通常在数据库中的密码都会进行加密后存储,可以使用Spring Security 来实现自定义的加密规则。要实现自定义加密规则,可以通过实现 `org.springframework.security.crypto.password.PasswordEncoder`接口,设置自定义的加密规则。 实现该接口需实现两个方法 * encode()方法对明文进行加密。 * matches()方法对明文加密后和密文进行比较。 ```java @Component public class PasswordEncoderServiceImpl implements PasswordEncoder { public static void main(String[] args) { System.out.println(new PasswordEncoderServiceImpl().privateEncode("123456")); } /** * 密码加密 * @param rawPassword 原始密码 */ @Override public String encode(CharSequence rawPassword) { return privateEncode(rawPassword); } /** * 密码匹配 * @param rawPassword 原始密码 * @param encodedPassword 加密的密码,通常从数据库中取得 * @return 匹配结果 */ @Override public boolean matches(CharSequence rawPassword, String encodedPassword) { // 1.对明文密码加密 String formPassword = privateEncode(rawPassword); // 2. 声明数据库密码 String databasePassword = encodedPassword; // 3. 比较密码 return Objects.equals(formPassword, databasePassword); } /** * 自定义加密规则 * @param rawPassword 原始密码 * @return 加密后的密码 */ private String privateEncode(CharSequence rawPassword){ String algorithm = "MD5"; try { // 1. 创建MessagDigest对象 MessageDigest messageDigest = MessageDigest.getInstance(algorithm); // 2. 获取rawPassword的字节数组 byte[] input = ((String)rawPassword).getBytes(); // 3. 加密 byte[] output = messageDigest.digest(input); // 4. 转为16进制的对应字符 return new BigInteger(1, output).toString(16).toUpperCase(); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); return null; } } } ``` 在配置类的configure 方法中应用自定义加密规则 ```java @Autowired private PasswordEncoderServiceImpl passwordEncoderService; @Override protected void configure(AuthenticationManagerBuilder builder) throws Exception { builder.userDetailsService(detailsService).passwordEncoder(passwordEncoderService); } ``` [commit:**自定义加密规则**](https://gitee.com/Sunxz007/SpringSecruityNotes/commit/42199d13030bdfebd13c4cd73f58e67a80d824fc) **问题** Md5 加密是对明文的直接加密,密文和明文是一一对应的,因此一旦数据库泄露了,密码也会被反编译出来,因此需要对md5加密出的密文进行进一步的加密 ### 盐值加密 我们可以可以使用自定义的盐值添加到明文中,进行多次的md5加密来解决md5加密撞库的问题 SpringSecurity 提供的 BCryptPasswordEncoder 加密规则。可以帮助进行更高级的加密算法 **使用方法** BCryptPasswordEncoder 创建对象后代替自定义 passwordEncoder 对象即可 。 BCryptPasswordEncoder 在加密时通过加入随机**盐值**让每一次的加密结果都不同。能够 避免密码的明文被猜到。 而在对明文和密文进行比较时,BCryptPasswordEncoder 会在密文的固定位置取出盐值,重新进行加密。 在spring 中注入 BCryptPasswordEncoder 替换自定义的passwordEncoder ```java @Autowired private BCryptPasswordEncoder passwordEncoder; @Bean public BCryptPasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } @Override protected void configure(AuthenticationManagerBuilder builder) throws Exception { builder.userDetailsService(detailsService).passwordEncoder(passwordEncoder); } ``` [commit:**BCryptPasswordEncoder 替换自定义的passwordEncoder**](https://gitee.com/Sunxz007/SpringSecruityNotes/commit/54e55357785ed29d345e200146cacd269fdfc2bc)