BiPerler 2020-06-03
前面学习了 security的登录与登出 , 但是用户信息 是 application 配置 或内存直接注入进去的 ,不具有实用性,实际上的使用还需要权限管理,有些 访问接口需要某些权限才可以使用
于是多了个权限管理的问题
spring boot 2.1.6.RELEASE
mysql 5.5.28*win64
jdk 1.8.0_221
(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
(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】
