spring security +MySQL + BCryptPasswordEncoder 单向加密验证 + 权限拦截 --- 心得
1.前言
前面学习了 security的登录与登出 , 但是用户信息 是 application 配置 或内存直接注入进去的 ,不具有实用性,实际上的使用还需要权限管理,有些 访问接口需要某些权限才可以使用
于是多了个权限管理的问题
2.环境
spring boot 2.1.6.RELEASE
mysql 5.5.28*win64
jdk 1.8.0_221
3.操作
(1)准备一张MySQL表
CREATE TABLE `t_user` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT ‘主键,自递增‘, `username` varchar(20) DEFAULT NULL COMMENT ‘用户名‘, `psw` varchar(140) DEFAULT NULL COMMENT ‘密码‘, `nickname` varchar(50) DEFAULT NULL COMMENT ‘别名‘, `role` varchar(100) DEFAULT NULL COMMENT ‘权限名‘, `setTime` datetime DEFAULT NULL COMMENT ‘注册时间‘, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4;
(2)目录结构
(3)pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.6.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.example</groupId> <artifactId>security-5500</artifactId> <version>0.0.1-SNAPSHOT</version> <name>security-5500</name> <description>Demo project for Spring Boot</description> <properties> <!-- 设置项目编码格式--> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <!--spring security 依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--访问静态资源--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <!-- MySQL 依赖--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <!-- <scope>runtime</scope>--> <version>5.1.30</version> </dependency> <!--MySQL 数据源 依赖包--> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.10</version> </dependency> <!-- mybatis依赖--> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.2</version> </dependency> <!-- mybatis的逆向工程依赖包--> <dependency> <groupId>org.mybatis.generator</groupId> <artifactId>mybatis-generator-core</artifactId> <version>1.3.2</version> </dependency> <!-- SCryptPasswordEncoder 加密才需要使用--> <dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcprov-jdk15on</artifactId> <version>1.64</version> </dependency> <!--java工具包--> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.9</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
(4)配置mybatis 与 dao层接口【具体操作这里不演示,可看我的其他随笔有具体讲解】
(5)配置前端页面
index.html
<!DOCTYPE html> <html lang="zh" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3"> <head> <meta charset="UTF-8"> <title>index</title> </head> <body> 你好 ,世界 ,2333 <p>点击 <a th:href="@{/home}">我</a> 去home.html页面</p> </body> </html>
home.html
<!DOCTYPE html> <html lang="zh" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3"> <head> <meta charset="UTF-8"> <title>security首页</title> </head> <body> <h1>Welcome!你好,世界</h1> <p>Click <a th:href="@{/hai}">here</a> to see a greeting.</p> </body> </html>
hai.html
<!DOCTYPE html> <html lang="zh" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3"> <head> <meta charset="UTF-8"> <title>hai文件</title> </head> <body> 你好呀世界,成功登录进来了 <br> <hr> 用户名:<span th:text="${username}"></span> <hr> <!-- 登出 路径是在security 拦截规则 那 设置的 ,当然也可以使用自己写的 ,必须post方式才可以访问,因为默认开启了CSRF --> <form th:action="@{/mylogout}" method="post"> <button class="btn btn-danger" style="margin-top: 20px">退出登录</button> </form> </body> </html>
kk.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>kk</title> </head> <body> <img src="img/xx.png" > </body> </html>
login.html
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3"> <head> <title>Spring Security自定义</title> </head> <body> <div th:if="${param.error}"> Invalid username and password. </div> <div th:if="${param.logout}"> You have been logged out. </div> <form th:action="@{/login}" method="post"> <div><label> User Name : <input type="text" name="username"/> </label></div> <div><label> Password: <input type="password" name="password"/> </label></div> <div><input type="submit" value="Sign In"/></div> </form> <br> lalallalalal啊是德国海 </body> </html>
(6)配置controller 虚拟路径 【访问接口】
package com.example.security5500.controller; import org.springframework.security.access.annotation.Secured; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.ModelAndView; import java.security.Principal; @Controller public class MVCController { @RequestMapping("/home") public String home() { return "home"; } @RequestMapping("/login") public String login(){ return "login"; } @RequestMapping("/hai") public String hai(@AuthenticationPrincipal Principal principal, Model model) { //获取登录用户名信息 ,如果没有登录 principal.getName() 会报异常,因此弄个异常抛出 String s= "r"; try { if (principal.getName() !=null){ s = principal.getName(); } }catch (Exception e){ System.out.println("principal.getName()出异常"); } model.addAttribute("username", s); return "hai"; } @RequestMapping({"/", "/index"}) public String index() { return "index"; } @RequestMapping("kk") public String kk() { return "kk"; } //获取用户权限 @RequestMapping({"/info"}) @ResponseBody public Object info(@AuthenticationPrincipal Principal principal) { return principal; } /* {"authorities":[{"authority":"admin"},{"authority":"user"}], "details":{"remoteAddress":"0:0:0:0:0:0:0:1","sessionId":"1F57B8E39C5D1DB1F875D57D533DB982"}, "authenticated":true,"principal":{"password":null,"username":"xi","authorities":[{"authority":"admin"}, {"authority":"user"}],"accountNonExpired":true,"accountNonLocked":true, "credentialsNonExpired":true,"enabled":true},"credentials":null,"name":"xi"} */ }
package com.example.security5500.controller; import com.example.security5500.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.Authentication; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.security.Principal; import java.util.Map; @Controller @RequestMapping("/admin") public class UserController { @Autowired private UserService userService; // //登出操作 // @RequestMapping({"/lo"}) // public String logout(HttpServletRequest request, HttpServletResponse response) { // Authentication auth = SecurityContextHolder.getContext().getAuthentication(); // if (auth != null) {//清除认证 // new SecurityContextLogoutHandler().logout(request, response, auth); // } // //重定向到指定页面 // return "redirect:/login"; // } //添加用户 @RequestMapping({"/addUser"}) @ResponseBody public Map<String,Object> addUser(String username , String psw ) { return userService.addUser(username,psw); } }
(7)service层实现类
package com.example.security5500.service.serviceImpl; import com.example.security5500.dao.TUserMapper; import com.example.security5500.entitis.tables.TUser; import com.example.security5500.service.UserService; import org.apache.commons.lang3.StringUtils; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.util.Date; import java.util.HashMap; import java.util.Map; @Service public class UserServiceImpl implements UserService { @Resource private TUserMapper tUserMapper; //根据用户名获取用户信息 @Override public TUser getByUsername(String useranme) { return tUserMapper.selectByUsername(useranme); } //添加新用户 @Override public Map<String,Object> addUser(String username, String psw) { Map<String,Object> map = new HashMap<>(); if (StringUtils.isBlank(username) || StringUtils.isBlank(psw)) { map.put("data","参数不可空"); return map; } ////根据用户名获取用户信息 TUser u = tUserMapper.selectByUsername(username); if (u!= null){ map.put("data","用户名已经存在"); return map; } // TUser tUser = new TUser(); tUser.setUsername(username); // //BCryptPasswordEncoder 单向加密 tUser.setPsw((new BCryptPasswordEncoder()).encode(psw)); // tUser.setNickname("别名-昵称"); tUser.setRole("user"); tUser.setSettime(new Date()); int len = tUserMapper.insertSelective(tUser); if (len!=1){ map.put("data","失败"); }else { map.put("data","成功"); } return map; } }
(8)启动类
package com.example.security5500; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.web.servlet.config.annotation.EnableWebMvc; @SpringBootApplication //设置mapper接口包位置 @MapperScan(basePackages = "com.example.security5500.dao") public class Security5500Application { public static void main(String[] args) { SpringApplication.run(Security5500Application.class, args); } }
(9)security配置类 ,继承了 WebSecurityConfigurerAdapter ,重写了父类方法 ,可对访问路径自定义设置拦截规则
package com.example.security5500.securityConfig; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Component; //这个加不加无所谓 //@Configuration //开启security自定义配置 @EnableWebSecurity //开启 Controller层的访问方法权限,与注解@PreAuthorize("hasRole(‘admin‘)")配合,但是 经测试,无法使用,前端访问指定接口报错403 , //@EnableGlobalMethodSecurity(prePostEnabled=true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { //实例自定义登录校验接口 【内部有 数据库查询】 @Autowired private DbUserDetailsService dbUserDetailsService; //拦截规则设置 @Override protected void configure(HttpSecurity http) throws Exception { http //允许基于使用HttpServletRequest限制访问 .authorizeRequests() //设置不拦截页面,可直接通过,路径访问 "/", "/index", "/home" 则不拦截, .antMatchers("/", "/index", "/home", "/hhk/**") //是允许所有的意思 .permitAll() //访问 /hai 需要admin权限 ,无权限则提示 403 .antMatchers("/hai").hasAuthority("admin") //访问 /kk 需要admin或user权限 ,无权限则提示 403 .antMatchers("/kk").hasAnyAuthority("admin","user") //路径/admin/**所有的请求都需要admin权限 ,无权限则提示 403 .antMatchers("/admin/**").hasAuthority("admin") //其他页面都要拦截,【需要在最后设置这个】 .anyRequest().authenticated() .and() //设置自定义登录页面 .formLogin() //指定自定义登录页面的访问虚拟路径 .loginPage("/login") .permitAll() .and() // 添加退出登录支持。当使用WebSecurityConfigurerAdapter时,这将自动应用。默认情况是,访问URL”/ logout”,使HTTP Session无效 // 来清除用户,清除已配置的任何#rememberMe()身份验证,清除SecurityContextHolder,然后重定向到”/login?success” .logout() // //指定的登出操作的虚拟路径,需要以post方式请求这个 http://localhost:5500/mylogout 才可以登出 ,也可以直接清除用户认证信息达到登出目的 .logoutUrl("/mylogout") //登出成功后访问的地址 .logoutSuccessUrl("/home"); } /** * 添加 UserDetailsService, 实现自定义登录校验,数据库查询 */ @Override protected void configure(AuthenticationManagerBuilder builder) throws Exception { //注入用户信息,每次登录都会来这查询一次信息,因此不建议每次都向mysql查询,应该使用redis //密码加密 builder.userDetailsService(dbUserDetailsService).passwordEncoder(passwordEncoder()); } /** * BCryptPasswordEncoder相关知识: * 用户表的密码通常使用MD5等不可逆算法加密后存储,为防止彩虹表破解更会先使用一个特定的字符串(如域名)加密,然后再使用一个随机的salt(盐值)加密。 * 特定字符串是程序代码中固定的,salt是每个密码单独随机,一般给用户表加一个字段单独存储,比较麻烦。 * BCrypt算法将salt随机并混入最终加密后的密码,验证时也无需单独提供之前的salt,从而无需单独处理salt问题。 */ @Bean public BCryptPasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } // /** // * 选择加密方式 ,密码不加密的时候选择 NoOpPasswordEncoder,不可缺少,否则报错 // * java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null" // */ // @Bean // public static PasswordEncoder passwordEncoder() { // return NoOpPasswordEncoder.getInstance(); // } }
(10)实现自定义登录校验,实现了根据用户名去数据库查询用户信息,集齐参数用户名、加密后的密码、权限 ,
然后使用 new org.springframework.security.core.userdetails.User(tUser.getUsername(), tUser.getPsw(), simpleGrantedAuthorities); 注册登录用户 ,
然后内部会自动对比密码 进行校验 【使用 BCryptPasswordEncoder 单项加密】
package com.example.security5500.securityConfig; import com.example.security5500.entitis.tables.TUser; import com.example.security5500.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.List; @Service public class DbUserDetailsService implements UserDetailsService { @Autowired private UserService userService; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { //根据用户名查询用户信息 TUser tUser = userService.getByUsername(username); if (tUser == null){ throw new UsernameNotFoundException("用户不存在!"); } //权限设置 // List<GrantedAuthority> simpleGrantedAuthorities = new ArrayList<>(); List<SimpleGrantedAuthority> simpleGrantedAuthorities = new ArrayList<>(); String role = tUser.getRole(); //分割权限名称,如 user,admin String[] roles = role.split(","); System.out.println("添加权限"); for (String r :roles){ System.out.println(r); //添加权限 simpleGrantedAuthorities.add(new SimpleGrantedAuthority(r)); } // simpleGrantedAuthorities.add(new SimpleGrantedAuthority("USER")); /** * 创建一个用于认证的用户对象并返回,包括:用户名,密码,角色 */ //输入参数 return new org.springframework.security.core.userdetails.User(tUser.getUsername(), tUser.getPsw(), simpleGrantedAuthorities); } }
(11)application.properties
spring.application.name=security-5500 # 应用服务web访问端口 server.port=5500 #配置security登录账户密和密码 ,不配置则默认账户是user,密码是随机生成的字符串,打印在启动栏中 #spring.security.user.name=11 #spring.security.user.password=22 # ## ## ## ## Enable template caching. #spring.thymeleaf.cache=true ## Check that the templates location exists. #spring.thymeleaf.check-template-location=true ## Content-Type value. ##spring.thymeleaf.content-type=text/html ## Enable MVC Thymeleaf view resolution. #spring.thymeleaf.enabled=true ## Template encoding. #spring.thymeleaf.encoding=utf-8 ## Comma-separated list of view names that should be excluded from resolution. #spring.thymeleaf.excluded-view-names= ## Template mode to be applied to templates. See also StandardTemplateModeHandlers. #spring.thymeleaf.mode=HTML5 ## Prefix that gets prepended to view names when building a URL. ##设置html文件位置 #spring.thymeleaf.prefix=classpath:/templates/ ## Suffix that gets appended to view names when building a URL. #spring.thymeleaf.suffix=.html spring.thymeleaf.template-resolver-order= # Order of the template resolver in the chain. spring.thymeleaf.view-names= # Comma-separated list of view names that can be resolved. # # #设置mybatis #mybatis设置 #mybatis配置文件所在路径 mybatis.config-location=classpath:mybatis/config/mybatisConfig.xml #所有Entity别名类所在包 mybatis.type-aliases-package=com.example.security5500.entitis.tables #mapper映射xml文件[也可以放在 resources 里面] #不论放在哪里,都必须使用classpath: 否则找不到 ,报错 org.apache.ibatis.binding.BindingException: Invalid bound statement (not found): mybatis.mapper-locations= classpath:mybatis/mapper/**/*.xml #mysql配置 # 当前数据源操作类型 spring.datasource.type=com.alibaba.druid.pool.DruidDataSource # mysql驱动包 spring.datasource.driver-class-name=org.gjt.mm.mysql.Driver # 数据库名称 spring.datasource.url=jdbc:mysql://localhost:3306/security?characterEncoding=utf-8 # 数据库账户名 spring.datasource.username=root # 数据库密码 spring.datasource.password=mysql # # # 数据库连接池的最小维持连接数 spring.datasource.dbcp2.min-idle=5 # 初始化连接数 spring.datasource.dbcp2.initial-size=5 # 最大连接数 spring.datasource.dbcp2.max-total=5 # 等待连接获取的最大超时时间 spring.datasource.dbcp2.max-wait-millis=200 # # 指明是否在从池中取出连接前进行检验,如果检验失败, 则从池中去除连接并尝试取出另一个, #注意: 设置为true后如果要生效,validationQuery参数必须设置为非空字符串 spring.datasource.druid.test-on-borrow=false # # 指明连接是否被空闲连接回收器(如果有)进行检验.如果检测失败,则连接将被从池中去除. #注意: 设置为true后如果要生效,validationQuery参数必须设置为非空字符串 spring.datasource.druid.test-while-idle=true # # 指明是否在归还到池中前进行检验,注意: 设置为true后如果要生效, #validationQuery参数必须设置为非空字符串 spring.datasource.druid.test-on-return=false # # SQL查询,用来验证从连接池取出的连接,在将连接返回给调用者之前. #如果指定,则查询必须是一个SQL SELECT并且必须返回至少一行记录 spring.datasource.druid.validation-query=select 1
4.测试
(1)启动 默认进入 index.html
点击 “我” ,进入 home.html
点击 “here” ,进入 hai.html ,但是因为设置了拦截,需要登录才可以访问 ,因此进入了自定义的登录页面
用一个只有 user权限的账户
username = cen
password = 11
登录后显示 403
因为我将访问 hai.html的权限设为需要 admin 才可以访问 ,因此拒绝操作
换一个有admin权限的账户
username = xi
password = 11
访问网址http://localhost:5500/login
再次登录
这是对一个终端访问接口的权限拦截
那么,需要将某一路径的请求都给拦截怎么办?难道一个一个写?
不,可以拦截上一层的虚拟路径
security的的配置写法
(2)一个拦截路径可以设置多个权限,只要有任意一个权限都可以访问
网址访问 http://localhost:5500/kk ,【无权限仍然提示403】