我们在篇(一)中已经谈到了默认的登录页面以及默认的登录账号和密码。

在这一篇中我们将自己定义登录页面及账号密码。

我们先从简单的开始吧:设置自定义的账号和密码(并非从数据库读取),虽然意义不大。

上一篇中,我们仅仅重写了 configure(HttpSecurity http) 方法,该方法是用于完成用户授权的。

为了完成自定义的认证,我们需要重写 configure(AuthenticationManagerBuilder auth) 方法。

public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// TODO Auto-generated method stub
auth.inMemoryAuthentication().withUser("Hello").password("{noop}World").roles("USER");
} @Override
protected void configure(HttpSecurity http) throws Exception {
// TODO Auto-generated method stub
http.authorizeRequests()
.antMatchers("/login").permitAll()
.antMatchers("/user").hasRole("USER")
.anyRequest().authenticated()
.and()
.formLogin().defaultSuccessUrl("/hello");
}
}

这个就是新的 WebSecurityConfig 类,控制器里面的方法我就不写了,仿照(一)很容易写出来,运行结果你们自己测试吧。

configure(AuthenticationManagerBuilder auth) 方法中,AuthenticationManagerBuilder 的 inMemoryAuthentication() 方法

可以添加用户,并给用户指定权限,它还有其他的方法,我们以后用到再讲。

在 Password 的地方我们需要注意了:

Spring 5.0 之后为了更加安全,修改了密码存储格式,密码存储格式为{id}encodedPassword。

id 是一个标识符,用于查找是哪个 PasswordEncoder,也就是密码加密的格式所对应的 PasswordEncoder。

encodedPassword 是指原始密码经过加密之后的密码。id 必须在密码的开始,id前后必须加 {}。

如果 id 找不到,id 则会为空,会抛出异常:There is no PasswordEncoder mapped for id "null"。

好啦,重点来啦,我们现在开始设置自定义登录页面,并从数据库读取账号密码。

一般来讲,我们先讲认证原理及流程比较好,不过这个地方我也说不太清楚。那我们还是从例子说起吧。

我用的是 MyBaits 框架操作 Mysql 数据库。为了支持它们,我们需要在原来的 pom.xml 中添加依赖。

<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.0</version>
</dependency> <dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>

好啦,现在我们首先定义一个用户对象以及一个角色对象。

package security.pojo;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails; public class SimpleUser implements UserDetails { private static final long serialVersionUID = 1L;
private String username;
private String password;
private String name;
private String telephone;
private String email;
private String headImg;
private boolean status = true;
private Set<Role> roles; public SimpleUser() {
super();
} public SimpleUser(String username, String password, String telephone) {
super();
this.username = username;
this.password = password;
this.telephone = telephone;
} public Set<Role> getRoles() {
return roles;
} public void setRoles(Set<Role> roles) {
this.roles = roles;
} @Override
public Collection<? extends GrantedAuthority> getAuthorities() {
// TODO Auto-generated method stub
if(!roles.isEmpty()) {
List<GrantedAuthority> authorities = new ArrayList<>();
for (Role role : roles) {
authorities.add(new SimpleGrantedAuthority("ROLE_" + role.getName()));
}
return authorities;
}
return null;
} @Override
public String getPassword() {
// TODO Auto-generated method stub
return password;
} @Override
public String getUsername() {
// TODO Auto-generated method stub
return username;
} @Override
public boolean isAccountNonExpired() {
// TODO Auto-generated method stub
return true;
} @Override
public boolean isAccountNonLocked() {
// TODO Auto-generated method stub
return true;
} @Override
public boolean isCredentialsNonExpired() {
// TODO Auto-generated method stub
return true;
} @Override
public boolean isEnabled() {
// TODO Auto-generated method stub
return status;
} public String getTelephone() {
return telephone;
} public void setTelephone(String telephone) {
this.telephone = telephone;
} public String getEmail() {
return email;
} public void setEmail(String email) {
this.email = email;
} public boolean getStatus() {
return status;
} public void setStatus(boolean status) {
this.status = status;
} public void setUsername(String username) {
this.username = username;
} public void setPassword(String password) {
this.password = password;
} public String getName() {
return name;
} public void setName(String name) {
this.name = name;
} public String getHeadImg() {
return headImg;
} public void setHeadImg(String headImg) {
this.headImg = headImg;
}
}

package security.pojo;

public class Role {

    private String username;
private String name; public Role() {
super();
}
public Role(String username, String name) {
super();
this.username = username;
this.name = name;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
} }

然后,为了根据用户名找到用户,我们定义 Mapper:

package security.mapper;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Insert;
import security.pojo.SimpleUser; @Mapper
public interface SimpleUserMapper { @Select("select * from users where username = #{username}")
public SimpleUser findUserByUsername(String username); @Insert("insert into users(username,password,telephone) values(#{username},#{password},#{telephone})")
public int addSimpleUser(SimpleUser user); }

package security.mapper;

import java.util.Set;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import security.pojo.Role; @Mapper
public interface RoleMapper { @Select("select * from roles where username = #{username}")
public Set<Role> findRolesByUsername(String username);
}

而这样的一个 Mapper 是不会加载到 Bean 中去的,我们需要对这个类进行扫描:

package security;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication
@MapperScan("security.mapper")
public class SecurityApplication { public static void main(String[] args) {
SpringApplication.run(SecurityApplication.class, args);
} }

好啦,这个 Mapper 已经成为一个 Bean 了,下面的将是重点:来自 《Spring Boot 2 企业应用实战》

1、UserDetails

UserDetails 是 Spring Security 的一个核心接口。其中定义了一些可以获取用户名、密码、权限等与认证相关信息的方法。

   Spring Security 内部使用的 UserDetails 实现类大都是内置的 User 类,要使用 UserDetails,也可以直接使用该类。

在 Spring Security 内部,很多需要使用用户信息的时候,基本上都是使用 UserDetails,比如在登录认证的时候。

UserDetails 是通过 UserDetailsService 的 loadUserByUsername() 方法进行加载的。

我们也需要实现自己的 UserDetailsService 来加载自定义的 UserDetails 信息。

2、UserDetailsService

Authentication.getPrincipal() 的返回类型是 Object,但很多情况下返回的其实是一个 UserDetails 的实例。

   登录认证的时候 Spring Security 会通过 UserDetailsService 的 loadByUsername() 方法获取相对应的 UserDetails

进行认证,认证通过后会将改 UserDetails 赋给认证通过的 Authentication 的 principal,

   然后再把该 Authentication 存入 SecurityContext。之后如果需要使用用户信息,

可以通过 SecurityContextHolder 获取存放在 SecurityContext 中的 Authentication 的 principal。

3、Authentication

Authentication 用来表示用户认证信息,在用户登录认证之前,

Spring Security 会将相关信息封装为一个 Authentication

具体实现类的对象,在登录认证成功之后又会生成一个信息更全面、包含用户权限等信息的 Authentication 对象,

然后把它保存在 SpringContextHolder 所持有的 SecurityContext 中,供后续的程序进行调用,如访问权限的鉴定等。

4、SecurityContextHolder

SecurityContextHolder 是用来保存 SecurityContext 的。SecurityContext 中含有当前所访问系统的用户的详细信息。

默认情况下,SecurityContextHolder 将使用 ThreadLocal 来保存 SecurityContext。

这也就意味着在处于同一线程的方法中,可以从 ThreadLocal 获取到当前 SecurityContext。

好啦,这个地方就到这儿啦,没弄懂也不要紧,我们能看懂例子就行了:

package security.service;

import java.util.Set;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import security.mapper.RoleMapper;
import security.mapper.SimpleUserMapper;
import security.pojo.Role;
import security.pojo.SimpleUser; @Service
public class SimpleUserService implements UserDetailsService { @Autowired
private SimpleUserMapper userMapper;
@Autowired
private RoleMapper roleMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// TODO Auto-generated method stub
SimpleUser user = userMapper.findUserByUsername(username);
Set<Role> roles = roleMapper.findRolesByUsername(username);
if(user == null) {
throw new UsernameNotFoundException("Username or Password is not correct");
}
user.setRoles(roles);
return new User(user.getUsername(),user.getPassword(),user.getAuthorities());
} public int addSimpleUser(SimpleUser user) {
user.setPassword(new BCryptPasswordEncoder().encode(user.getPassword()));
return userMapper.addSimpleUser(user);
} }

在这个类中,我们实现了 UserDetailsService 接口,然后重写了 loadUserByUsername(String username) 方法。

之后自动注入了一个根据用户名查找用户的 Mapper,再将查找的用户对象复制给 user。

当存在这个用户的时候,我们获取它的权限添加到权限列表中,然后把这个列表以及用户名,密码存入到 UserDetails 对象中。

因为一个用户的权限可能不止一个,所以是一个权限列表。

最后我们到了配置环节了:

package security.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import security.service.SimpleUserService;
// 重写DaoAuthenticationProvider,authentication 携带username,password信息
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired
private SimpleUserService userService;
@Autowired
private AuthenticationProvider authenticationProvider;
private MessageSource messageSource;
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// TODO Auto-generated method stub
auth.authenticationProvider(authenticationProvider);
} @Override
protected void configure(HttpSecurity http) throws Exception {
// TODO Auto-generated method stub
http
.authorizeRequests()
.anyRequest().permitAll()
.and()
.formLogin().loginPage("/signin")
.usernameParameter("username")
.passwordParameter("password")
.loginProcessingUrl("/signin")
.and()
.csrf().disable();
} @Bean
public AuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider provider = new CustomAuthenticationProvider();
provider.setMessageSource(messageSource);
provider.setUserDetailsService(userService);
provider.setPasswordEncoder(new BCryptPasswordEncoder());
return provider;
}
}

package security.config;

import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.util.Assert; public class CustomAuthenticationProvider extends DaoAuthenticationProvider { @Override
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
// TODO Auto-generated method stub String presentedPassword = authentication.getCredentials().toString();
if (!getPasswordEncoder().matches(presentedPassword, userDetails.getPassword())) {
logger.debug("Authentication failed: password does not match stored value"); throw new BadCredentialsException(messages.getMessage(
"UNameOrPwdIsError","Username or Password is not correct"));
}
} @Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// TODO Auto-generated method stub
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
() -> messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported")); if("".equals(authentication.getPrincipal())) {
throw new BadCredentialsException(messages.getMessage(
"UsernameIsNull","Username cannot be empty"));
}
if("".equals(authentication.getCredentials())) {
throw new BadCredentialsException(messages.getMessage(
"PasswordIsNull","Password cannot be empty"));
} String username = (String) authentication.getPrincipal();
boolean cacheWasUsed = true;
UserDetails user = this.getUserCache().getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException notFound) {
logger.debug("User '" + username + "' not found"); if (hideUserNotFoundExceptions) {
throw new BadCredentialsException(messages.getMessage(
"UNameOrPwdIsError","Username or Password is not correct"));
}
else {
throw notFound;
}
}
Assert.notNull(user,
"retrieveUser returned null - a violation of the interface contract");
}
try {
getPreAuthenticationChecks().check(user);
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException exception) {
if (cacheWasUsed) {
cacheWasUsed = false;
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
getPreAuthenticationChecks().check(user);
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
else {
throw exception;
}
} getPostAuthenticationChecks().check(user); if (!cacheWasUsed) {
this.getUserCache().putUserInCache(user);
} Object principalToReturn = user; if (isForcePrincipalAsString()) {
principalToReturn = user.getUsername();
} return createSuccessAuthentication(principalToReturn, authentication, user);
} }

package security.config;

import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.context.support.ResourceBundleMessageSource; public class MessageSource extends ResourceBundleMessageSource { public MessageSource() {
setBasename("messages");
} public static MessageSourceAccessor getAccessor() {
return new MessageSourceAccessor(new MessageSource());
} }

在第一个类中我们重写了两个 configure() 方法。其中一个我们之前谈过,不过并没有讲全,现在补充一下:

在 formLogin() 下还有 .usernameParameter() 和 .passwordParameter() 以及 .loginProcessingUrl("/login") 这三个函数。

前两个函数是用于指定登录页面用户名及密码的标识的,后面的一个是用于表单请求的 action 参数。

defaultSuccessUrl 是指定登录成功显示的页面,failureUrl 是指定登录失败显示的页面。

还有其他的一些我们以后用到再讲。

另一个 configure() 方法是用于认证的。我们这里仅仅只写了一行代码。

我们把之前的 @Service 的那个类注入到了 userService 中,再把 @Bean 的那个 Bean 注入到了 authenticationProvider 中。

在这个 Bean 里面有个 DaoAuthenticationProvider 类:

Spring Security 默认会使用 DaoAuthenticationProvider 实现 AuthenticationProvider 接口,专门进行用户认证处理。

DaoAuthenticationProvider 在进行认证处理的时候需要一个 UserDetailsService 来获取用户的信息 UserDetails,

其中包括用户名,密码和所拥有的权限等。

看到这些代码,可以知道我们写的代码都有联系了。我们还差一个控制器的代码:

package security.controller;

import java.util.Random;
import security.pojo.SimpleUser;
import security.service.SimpleUserService;
import com.zhenzi.sms.ZhenziSmsClient;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam; @Controller
public class SecurityController { @Autowired
private SimpleUserService userService; @GetMapping("/signin")
public String signIn() {
return "signin";
} @GetMapping("/signup")
public String signUp() {
return "signup";
} @PostMapping("/sign_up")
public String regist(@RequestParam(value="verifycode") String code,HttpServletRequest request, SimpleUser user) {
String verifycode = (String) request.getSession().getAttribute("verifyCode");
if(!code.equals(verifycode)){
return "failure";
}
userService.addSimpleUser(user);
return "signin";
} @PostMapping("/sendsms")// 若不要 response 参数,则会发出 /sendsms 请求。
public void sendsms(HttpServletRequest request, HttpServletResponse response, String telephone) {
try {
String verifyCode = String.valueOf(new Random().nextInt(899999) + 100000);
ZhenziSmsClient client = new ZhenziSmsClient("******", "******", "******");
client.send(telephone, "您的验证码为 " + verifyCode + ",有效期为 3 分钟,如非本人操作,可不予理会!");
request.getSession().setAttribute("verifyCode", verifyCode);
} catch (Exception e) {
e.printStackTrace();
}
}
}

好啦,到此 java 代码就结束了。

前面我们设置了 usernameParameter("username"),passwordParameter("password"),

另外由于默认的登录页面表单请求的 action="/login",用户名参数和密码分别为 "username","password"。

public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
private String usernameParameter = "username";
private String passwordParameter = "password";
private boolean postOnly = true; public UsernamePasswordAuthenticationFilter() {
super(new AntPathRequestMatcher("/login", "POST"));
}
// ... ... }

如果用 thymeleaf 模板的话,这三个参数就分别用 th:action="{/login}" ,th:name="username",th:name="password"。

若是我们想自定义的话,比如登录页面为 signin.html,登录请求的 action 为 "/signin",

用户名参数为 uname,密码参数为 pwd。

    @Override
protected void configure(HttpSecurity http) throws Exception {
// TODO Auto-generated method stub
http.authorizeRequests()
.antMatchers("/css/**","/images/*","/js/**","/login").permitAll()
.antMatchers("/index").hasRole("USER")
.anyRequest().authenticated()
.and()
.formLogin().loginPage("/login")
.usernameParameter("uname")
.passwordParameter("pwd")
.loginProcessingUrl("/sign")
.defaultSuccessUrl("/success")
.failureUrl("/failure");
}

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<form th:action="@{/signin}" method="post">
<input th:name="uname" type="text">
<input th:name="pwd" type="password">
<input type="submit" value="login">
</form>
</body>
</html>

熬,对啦,连接数据库的地方需要写在 application.properties 文件里:

注意了,那个 url 数据库(security)后面一定要写上 ?serverTimezone=UTC&characterEncoding=utf-8 这样的,不然会出错的。

至此,入门项目就结束了,所有的源码都在上面啦,觉得可以的话点个赞啦!

链接:https://pan.baidu.com/s/13fc6P9NV49aRRBctr3MjNQ
提取码:4qgu

Spring Security 入门 (二)的更多相关文章

  1. SpringBoot集成Spring Security入门体验

    一.前言 Spring Security 和 Apache Shiro 都是安全框架,为Java应用程序提供身份认证和授权. 二者区别 Spring Security:重量级安全框架 Apache S ...

  2. Spring Security(二)

    Spring Security(二) 注:凡是源码部分,我已经把英文注释去掉了,有兴趣的同学可以在自己项目里进去看看.:-) 定义用户认证逻辑 用户登录成功后,用户的信息会被 Security 封装在 ...

  3. Spring Security 解析(二) —— 认证过程

    Spring Security 解析(二) -- 认证过程   在学习Spring Cloud 时,遇到了授权服务oauth 相关内容时,总是一知半解,因此决定先把Spring Security .S ...

  4. Spring Security教程(二):自定义数据库查询

    Spring Security教程(二):自定义数据库查询   Spring Security自带的默认数据库存储用户和权限的数据,但是Spring Security默认提供的表结构太过简单了,其实就 ...

  5. Spring Security 入门(基本使用)

    Spring Security 入门(基本使用) 这几天看了下b站关于 spring security 的学习视频,不得不说 spring security 有点复杂,脑袋有点懵懵的,在此整理下学习内 ...

  6. Spring Security 入门

    一.Spring Security简介 Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架.它提供了一组可以在Spring应用上下文中配 ...

  7. Spring Security 入门(一)

    当你看到这篇文章时,我猜你肯定是碰到令人苦恼的问题了,我希望本文能让你有所收获. 本人几个月前还是 Spring 小白,几个月走来,看了 Spring,Spring boot,到这次的 Spring ...

  8. Spring Security入门(2-3)Spring Security 的运行原理 4 - 自定义登录方法和页面

    参考链接,多谢作者: http://blog.csdn.net/lee353086/article/details/52586916 http元素下的form-login元素是用来定义表单登录信息的. ...

  9. Spring Security 入门(1-1)Spring Security是什么?

    1.Spring Security是什么? Spring Security 是一个安全框架,前身是 Acegi Security , 能够为 Spring企业应用系统提供声明式的安全访问控制. Spr ...

随机推荐

  1. android.intent.category.BROWSABLE

    参考: http://blog.csdn.net/annkie/article/details/8349626 http://xiechengfa.iteye.com/blog/1004991 BRO ...

  2. application.properties 乱码 (已验证)

    1.打开Eclipse或MyEclipse 2.选择window-Preferences-content Types-Text-Java Properties File 3.将Java Propert ...

  3. Webdriver元素定位的方法

    webdriver提供了8种元素定位方法: 1.id 2.name 3.tag name 4.class name 5.link text 6.partial link text 7.xpath 8. ...

  4. hbase数据备份或者容灾方案

    HBase的数据备份或者容灾方案有这几种:Distcp,CopyTable,Export/Import,Snapshot,Replication,以下分别介绍(以下描述的内容均是基于0.94.20版本 ...

  5. nfs 存储服务

    今日内容: NFS 1.什么是nfs? network file system 网络文件系统 nfs共享存储 2.nfs能干什么? nfs 能为 不同主机系统之间 实现 文件的共享 3.为什么要使用n ...

  6. 网站启动,报编译错误:类型“ASP.global_asax”同时存在两个文件夹的问题

    CS0433: The type 'ASP.global_asax' exists in both 'c:\Windows\Microsoft.NET\Framework64\v4.0.30319\T ...

  7. Docker Gitlab CI 部署 Spring Boot 项目

    目前在学习这一块的内容,但是可能每个人环境都不同,导致找不到一篇博客能够完全操作下来没有错误的,所以自己也写一下,记录一下整个搭建的过程. Docker 的安装这里就不赘述了,基本上几行命令都可以了, ...

  8. java取json 的方法

    public static void main(String[] args) { String jsonStr = "[{\"varieties_type\":\&quo ...

  9. Mybatis逆向工程过程中出现targetRuntime in context mybatisGenerator is invalid

    最开始设置的Mybatis,但是逆向工程准备就绪后出现问题 报错为targetRuntime in context mybatisGenerator is invalid 后来修改为Mybatis3能 ...

  10. 【SQL server】SQL Server 触发器

    触发器是一种特殊类型的存储过程,它不同于之前的我们介绍的存储过程.触发器主要是通过事件进行触发被自动调用执行的.而存储过程可以通过存储过程的名称被调用. Ø 什么是触发器 触发器对表进行插入.更新.删 ...