什么是Spring安全框架
Spring安全:Spring-Security
是Spring提供的安全管理框架,功能是提供一个安全可靠的登录功能,并且支持权限管理功能,而且自带判断当前用户是否登录的过滤器,如果用户没有登录会跳转到登录页面
为什么需要Spring-Security
使用Spring-Security框架能够使新手程序也能写出企业级别安全的登录功能
Spring-Security包含了权限管理的功能,能够方便的保存一个用户的各种权限,使用简单地方式判断这个用户是否包含这些权限,决定是否允许访问
能够帮助程序员提升编写登录和权限管理功能的开发效率
启动Spring-Security
启动Spring-Security非常的简单 只需要添加依赖即可
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
加好这个依赖Spring-Security这个框架就会在项目中生效了
现在所有项目中的资源都会被Spring-Security保护
也就是说默认情况下,要想访问当前项目的任何资源,都需要先登录。
而登录方法是: 用户名:user 密码:启动服务时idea控制台出现的随机密码
访问控制器方法
打开创建好的UserController在其中添加一个方法
代码如下:
@RestController
@RequestMapping("/v1/users")
public class UserController {
@GetMapping("/get")
public String get(){
return "Hello html";
}
}
重启服务,访问localhost:8080/v1/users/get 也是需要登录的,因为控制器的响应也属于网站资源,受到Spring-Security保护
密码加密
上面我们登录只能使用user这个用户,而且密码每次都要到控制台复制,比较麻烦
Spring-Security允许我们自定义的用户名和密码配置到application.properties中配置
#配置Spring-Security的自定义用户名和密码
spring.security.user.name=admin
spring.security.user.password=123456
但是这样配置的话,任何可以看到配置文件的人都可以登陆这个网站
所以我们需要学习密码加密,加密之后即使别人看到密码,也不能登录
我们可以使用市面上流星的安全加密算法:bcrypt
这个加密算法可以将任何数据进行加密保存,保证安全 在测试类中进行一个加密操作,代码如下:
@SpringBootTest
public class PasswordTest {
PasswordEncoder encoder = new BCryptPasswordEncoder();
@Test
public void test(){
String str = "123456";
String pwd = encoder.encode(str);
System.out.println(pwd);
}
}
运行输出了一个加密结果后发现每次结果都不同,因为每次加密秘结果相同的话安全性较低,bcrypt加密算法采用了"随机校验"技术,让每次生成结果都不同. 加密结果 加密完成下面进行验证的代码,bcrypt提供了验证的方法,可以判断一个字符串是否匹配一个加密结果
@Test
public void match(){
boolean b=encoder.matches("123456",
"$2a$10$B5Ba4G77NuxAcRJ/iucipOaXjc/3uranz.lMW008IVxRdG
BATv8d2");
System.out.println("匹配结果:"+b);
}
最终目的是将加密结果配置在配置文件中application.properties文件修改为
#配置Spring-Security的自定义用户名和密码
spring.security.user.name=admin
spring.security.user.password={bcrypt}$2a$10$.6XmtLGrTwxO/JWWJCoc4OjoBQ7RG6cZ1WEHtYNbbYQWzVaqjTj2i
Spring-Security的权限管理功能
我们最终的登录是要支持现有数据库中所有user的数据
现在只能支持配置文件中的用户
如果要实现数据库登录,首先要有机会在java代码中设置用户名密码
创建一个security包,包中创建SecurityConfig,代码如下:
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("tom")
.password("{bcrpt}$2a$10$.6XmtLGrTwxO/JWWJCoc4OjoBQ7RG6cZ1WEHtYNbbYQWzVaqjTj2i")
.authorities("test");
}
}
控制器方法可以设定当前方法需要什么特殊权限才能访问,如果不设置默认情况下登录就可以访问
修改UserController代码如下:
@RestController
@RequestMapping("/v1/users")
public class UserController {
@GetMapping("/get")
public String get(){
return "Hello html";
}
@GetMapping("/list")
@PreAuthorize("hasAuthority('run')")
public String list(){
return "get list";
}
}
实现数据库中的用户登录
通过之前的部分,现在知道在Java代码中,要想让用户登录至少要提供用户名,密码,和当前用户权限
数据库用户表中没有直接提供当前用户的权限,那么我们就要根据对当前用户的id查询当前用户的权限 这个查询可能涉及上面的5张表
因为用户对角色和角色对权限都是多对多
今后面试时如果问到权限数据的实现方式,需要回答上面的5张表
我们需要编写一个根据用户id查询所有权限的5表联查的sql语句
SELECT p.id , p.name
FROM user u
LEFT JOIN user_role ur ON u.id=ur.user_id
LEFT JOIN role r ON r.id=ur.role_id
LEFT JOIN role_permission rp ON r.id=rp.role_id
LEFT JOIN permission p ON p.id=rp.permission_id
WHERE u.id=11
我们需要在数据访问层编写这个方法,在登录业务中需要时调用
打开UserMapper编写代码如下
@Repository
public interface UserMapper extends BaseMapper<User> {
@Select("SELECT p.id , p.name\n" +
"FROM user u\n" +
"LEFT JOIN user_role ur ON u.id=ur.user_id\n" +
"LEFT JOIN role r ON r.id=ur.role_id\n" +
"LEFT JOIN role_permission rp ON r.id=rp.role_id\n" +
"LEFT JOIN permission p ON p.id=rp.permission_id\n" +
"WHERE u.id=#{id}")
List<Permission> findUserPermissionsById(Integer id);
@Select("select * from user where username=#{username}")
User findUserByUsername(String username);
}
有了用户名,密码和用户权限等信息
下面就可以按照Spring-Security规定方式进行登录代码的编写了
我们需要自己编写一个类,这个类实现Spring-Security提供的恶一个接口UserDetailsService,而这个接口中需要实现一个方法,这个方法的功能是根据用户输入在登录框中的用户名进行用户信息(用户名密码权限)的查询,返回值必须是UserDetails,我们需要将这个类型对象实例化后赋值最后返回以完成登录
在service.impl包中新建一个类UserDetailsServiceImpl
代码如下
@Component
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user= userMapper.findUserByUsername(username);
if (user==null){
return null;
}
List<Permission> permissions=
userMapper.findUserPermissionsById(user.getId());
String[] auth = new String[permissions.size()];
int i =0;
for (Permission p : permissions){
auth[i]=p.getName();
i++;
}
UserDetails details =
org.springframework.security.core.userdetails.User.builder()
.username(user.getUsername())
.password(user.getPassword())
.authorities(auth).accountLocked(user.getLocked()==1)
.disabled(user.getEnabled()==0)
.build();
return details;
}
}
上面的代码是Spring-Security要求我们编写的完成登录功能的代码
我们要想登录成功,还要将这个类型对象和Spring-Security建立关系
回到security包中的SecurityConfig类
将我们之前编写的configure方法修改为
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends
WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsServiceImpl userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
}
设置放行页面
当今流行的网站都是有些页面允许不登录就能访问
而我们现在的Spring-Security下所有资源都需要登录才能访问
我们如果想放行一些页面需要配置下面代码
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers(
"/index_student.html",
"/css/*",
"/js/*",
"/img/**",
"/bower_components/**")
.permitAll()
.anyRequest()
.authenticated()
.and()
.formLogin();
}
自定义登录页面
Spring-Security提供的默认登录页面不能体现当前网站的特征,也不能编写其他功能或连接,很受限制,我们希望能够使用自定义的login.html页面进行登录操作
也是需要进行对应的配置
SecurityConfig继续配置
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers(
"/index_student.html",
"/css/*",
"/js/*",
"/img/**",
"/bower_components/**",
"/login.html")
.permitAll()
.anyRequest()
.authenticated()
.and()
.formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/login")
.failureUrl("/login.html?error")
.defaultSuccessUrl("/index_student.html")
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/login.html?logout");
}
注册功能流程分析
注册业务流程
- 学生填写注册表单信息
- 提交注册信息到控制器
- 控制器接收到信息调佣业务逻辑层方法
- 业务逻辑层中判断邀请码,手机号并对密码加密后进行数据库新增
- mapper层执行新增方法,返回到业务逻辑层
- 业务逻辑层将注册结果返回给控制鞥
- 控制层将最终信息显示在页面上
注册业务准备
首设置注册页面和控制器路径的放行
打开SecurityConfig配置类,进行放行配置
http.csrf().disable()
.authorizeRequests()
.antMatchers(
"/index_student.html",
"/css/*",
"/js/*",
"/img/**",
"/bower_components/**",
"/login.html",
"/register.html",
"/register")
.permitAll()
.anyRequest()
.authenticated()
.and()
.formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/login")
.failureUrl("/login.html?error")
.defaultSuccessUrl("/index_student.html")
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/login.html?logout");
根据表单参数创建vo类
@Data
public class RegisterVo implements Serializable {
private String inviteCode;
private String phone;
private String nickname;
private String password;
private String confirm;
}
还需要自定义异常类
在我们编写的业务发生异常不能继续运行时,使用抛出异常的方式反馈 错误信息
我们定义一个自定义异常类,ServiceException
来表示业务逻辑运行过程中发生的各种不能继续运行程序的异常
例如:邀请码不正确\手机号已经被注册
新建一个包exception,新建类代码如下
public class ServiceException extends RuntimeException{
private int code = 500;
public ServiceException() { }
public ServiceException(String message) {
super(message);
}
public ServiceException(String message, Throwable
cause) {
super(message, cause);
}
public ServiceException(Throwable cause) {
super(cause);
}
public ServiceException(String message, Throwable
cause,
boolean enableSuppression,
boolean writableStackTrace) {
super(message, cause, enableSuppression,
writableStackTrace);
}
public ServiceException(int code) {
this.code = code;
}
public ServiceException(String message, int code) {
super(message);
this.code = code;
}
public ServiceException(String message, Throwable
cause,
int code) {
super(message, cause);
this.code = code;
}
public ServiceException(Throwable cause, int code)
{
super(cause);
this.code = code;
}
public ServiceException(String message, Throwable
cause,
boolean enableSuppression,
boolean writableStackTrace, int code) {
super(message, cause, enableSuppression,
writableStackTrace);
this.code = code;
}
public int getCode() {
return code;
}
}
还可以设置简单条件,适合偶尔一次查询数据库使用 我们在测试类中编写一个测试,按邀请码查询班级信息 代码如下
706246'
@Autowired
ClassroomMapper classroomMapper;
@Test
public void query(){
QueryWrapper<Classroom> query=new QueryWrapper<>();
query.eq("invite_code","JSD2001-70624");
体类型
Classroom
classroom=classroomMapper.selectOne(query);
System.out.println(classroom);
}
业务逻辑层概述
Vrd项目中完成一次请求响应流程一般会由两个部分组成 请求->控制器(controller)->数据访问层(mapper) 上面的执行流程只能处理相对简单的业务逻辑层 如果遇到企业中的相对复杂的业务逻辑就不能很好的处理了 每个类都应该有自己的职责 // 根据邀请码查询班级信息 // 如果写sql语句: // select * from classroom where invite_code=‘JSD2001- 706246’ // 如果使用QueryWrapper进行查询 代码如下 @Autowired ClassroomMapper classroomMapper; @Test public void query(){ // 我们实例化一个QueryWrapper的对象 // 这个对象其实就是代表查询的条件,泛型是实体类的类型 QueryWrapper query=new QueryWrapper<>(); // 设置查询条件 query.eq([列名],[值]) query.eq(“invite_code”,“JSD2001-70624”); // 按QueryWrapper对象设置好的条件进行查询的操作 // selectOne方法只支持最多返回1行数据,否则报错,返回值是实 体类型 Classroom classroom=classroomMapper.selectOne(query); System.out.println(classroom); } controller:职责就是接收前端页面的信息和将结果响应给页面,其他的事 情尽量不管 mapper:完成对数据库的增删改查操作,其他的操作也不管 如果出现了既不属于controller的也不属于mapper职责的工作,就需要 写在业务逻辑层中 service(业务逻辑层):职责就是将前端发送来的信息经过处理再调用数据 访问层的功能,例如我们接收了用户输入的邀请码但是需要判断是否正 确 企业标准中,service又由两个部分组成 service和service.impl service中保存业务逻辑层接口:一般命名为IXXXService(开头的I表示 Interface) service.impl中保存业务逻辑层实现类:一般命名为 XXXServiceImpl(Impl表示实现的缩写) 之所以采用接口配实现类的形式,是为了解耦 所以在需要业务逻辑层代码时,我们都声明接口类型 再今后我们开发程序的模型中,控制层,业务逻辑层,数据访问层这三层结 构如下
开发注册业务逻辑层代码
实际开发中,应该先完成数据访问层的编写,但是当前业务中,所有数据库 操作都是基本增删改查,已经由MybatisPlus提供了,所以Mapper层不需 要编写代码 先编写业务逻辑层接口 IUserService添加方法如下
public interface IUserService extends IService<User> {
void registerStudent(RegisterVo registerVo);
}
UserServiceImpl实现类代码如下
@Service
public class UserServiceImpl extends
ServiceImpl<UserMapper, User> implements IUserService {
@Autowired
private UserMapper userMapper;
@Autowired
private ClassroomMapper classroomMapper;
@Autowired
private UserRoleMapper userRoleMapper;
@Override
public void registerStudent(RegisterVo registerVo)
{
QueryWrapper<Classroom> query=new
QueryWrapper<>();
query.eq("invite_code",registerVo.getInviteCode());
Classroom
classroom=classroomMapper.selectOne(query);
if(classroom==null){
throw new ServiceException("邀请码错误!");
}
User user=userMapper.findUserByUsername(
registerVo.getPhone());
异常
if(user!=null){
throw new ServiceException("手机号已经被注
册!");
}
PasswordEncoder encoder=new
BCryptPasswordEncoder();
String pwd="
{bcrypt}"+encoder.encode(registerVo.getPassword());
User u=new User()
.setUsername(registerVo.getPhone())
.setNickname(registerVo.getNickname())
.setPassword(pwd)
.setClassroomId(classroom.getId())
.setCreatetime(LocalDateTime.now())
.setEnabled(1)
.setLocked(0)
.setType(0);
int num=userMapper.insert(u);
if(num!=1){
throw new ServiceException("数据库忙");
}
UserRole userRole=new UserRole()
.setUserId(u.getId())
.setRoleId(2);
num=userRoleMapper.insert(userRole);
if(num!=1){
throw new ServiceException("数据库忙");
}
}
}
推荐大家编写完比价复杂的业务逻辑代码时进行测试 代码如下
@Autowired
IUserService userService;
@Test
public void add(){
RegisterVo registerVo=new RegisterVo();
registerVo.setPhone("13033012345");
registerVo.setNickname("大龙");
registerVo.setInviteCode("JSD2001-706246");
registerVo.setPassword("123456");
userService.registerStudent(registerVo);
System.out.println("ok");
}
开发控制层代码
我们先来编写控制层接收表单信息的代码
创建SystemController类
编写代码如下
@RestController
@Slf4j
public class SystemController {
@Autowired
private IUserService userService;
@PostMapping("/register")
public String register(RegisterVo registerVo){
log.debug("接收到用户信息:{}",registerVo);
try {
userService.registerStudent(registerVo);
return "ok";
}catch (ServiceException e){
log.error("注册失败",e);
return e.getMessage();
}
}
}
重启服务测试
注册成功表示代码正确,检查数据库user表和user_role表示的信息
修改为异步测试
gitee同步更新中 将一个同步的注册修改为异步注册需要如下修改
- 页面中需要的支持(vue,axios等)
<script
src="https://unpkg.com/axios/dist/axios.min.js">
</script>
- 编写并引用js代码
</body>
<script src="js/utils.js"></script>
<script src="js/register.js"></script>
</html>
- 页面的vue绑定(v-model…)
34行开始
<form action="/register" method="post"
@submit.prevent="register">
<div class="form-group has-icon">
<input type="text" name="inviteCode" class="formcontrol" placeholder="请输入邀请码"
required="required" v-model="inviteCode">
<span class="fa fa-barcode form-control-icon">
</span>
</div>
<div class="form-group has-icon">
<input type="tel" name="phone" class="form-control"
placeholder="请输入手机号"
pattern="^\d{11}$" required="required"
v-model="phone">
<span class=" fa fa-phone form-control-icon">
</span>
</div>
<div class="form-group has-icon">
<input type="text" name="nickname" class="formcontrol" placeholder="请设置昵称,字数为2-20之间"
pattern="^.{2,20}$" required="required"
v-model="nickname">
<span class="fa fa-user form-control-icon"></span>
</div>
<div class="form-group has-icon">
<input type="password" name="password" class="formcontrol" placeholder="设置密码6-20个字母、数字、下划线"
required="required" pattern="^\w{6,20}$"
v-model="password">
<span class="fa fa-lock form-control-icon"></span>
</div>
<div class="form-group has-icon">
<input type="password" name="confirm" class="formcontrol" placeholder="请再次输入密码"
required="required"
v-model="confirm">
<span class="fa fa-lock form-control-icon"></span>
</div>
<button type="submit" class="btn btn-primary btnblock btn-flat" >注册</button>
</form>
我们需要修改一下注册页面显示错误信息的区域 30行附近
<div id="error" class="alert alert-danger"
style="display: none"
:class="{'d-block':hasError}">
<i class="fa fa-exclamation-triangle"></i>
<span v-text="message" >邀请码错误!</span>
</div>
register.js代码 if判断修改一下
.then(function(r) {
console.log("|"+r.status+"|"+OK+"|");
if(r.data=="ok"){
console.log("注册成功");
console.log(r.data);
app.hasError = false;
location.href = '/login.html?register';
}else{
console.log(r.data);
app.hasError = true;
app.message = r.data;
}
});
Spring验证框架
表单验证基本概念
我们页面中通过编写html5diamante可以实现表单验证的逻辑
一般情况下,非空正则表达式的验证都是被支持的
正常编写表单验证之后,可以降低服务器的压力,提高服务器的运行性能
表单眼中正式非常好的验证方案,但是也有缺点:它只能防止通过表单进行注册的用户提交错误信息
如果有人恶意绕开浏览器直接将请求信息发送给服务器。服务器就会处理错误信息,严重情况下服务器就会瘫痪,我们需要服务端进行验证
有了服务端验证,才能防止绕开浏览器的数据不经过验证就进入数据库
这样的验证我们自己写是比较繁琐的,我们需要框架帮助我们简化
什么是Spring验证框架
Spring验证框架:Spring Validation 它能实现简单方便的服务器端验证并且能够接收验证结果
Spring Validation的使用
要想使用先添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-startervalidation</artifactId>
</dependency>
添加完成依赖之后,刷新maven,到需要验证的类中边写正则表达式,也支持非空验证和正则表达式验证等其他验证方式
@Data
public class RegisterVo implements Serializable {
@NotBlank(message = "邀请码不能为空")
private String inviteCode;
@NotBlank(message = "手机号不能为空")
@Pattern(regexp = "^1\\d{10}$",message = "手机号格式
不正确")
private String phone;
@NotBlank(message = "昵称不能为空")
@Pattern(regexp = "^.{2,20}$",message = "昵称是2~20
位字符")
private String nickname;
@NotBlank(message = "密码不能为空")
@Pattern(regexp = "^\\w{6,20}$",message = "密码是
6~20位字符")
private String password;
@NotBlank(message = "确认密码不能为空")
private String confirm;
}
控制器启动验证
@PostMapping("/register")
public String register(
证功能
RegisterVo
@Validated RegisterVo registerVo,
BindingResult result){
log.debug("接收到用户信息:{}",registerVo);
if(result.hasErrors()){
String msg=result.getFieldError().
getDefaultMessage();
return msg;
}
try {
userService.registerStudent(registerVo);
return "ok";
}catch (ServiceException e){
log.error("注册失败",e);
return e.getMessage();
}
}
观察效果,我们还是使用浏览器来测试 所以可以暂时删除一些html5的表单验证
随笔
QueryWrapper方法含义:
eq(): equals-等于 gt(): grate than 大于 lt(): less than 小于 ge(): grate equals 大于等于 le(): 小于等于 ne(): not equals 不等于
|