目录:
1、默认表单认证
创建 springboot 项目,依赖:
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-security
写一个测试 controller
@RestController
@RequestMapping("/index")public classIndexController {
@RequestMapping("/test1")publicString test1(String name, Integer age) {return "test1";
}
}
启动项目,访问 http://localhost:8089/BootDemo/index/test1,弹出默认表单认证
默认用户名为 user, 密码是动态生成并打印到控制台的一窜随机码。当然,用户名和密码可以在application.properties 中配置
spring.security.user.name=test
spring.security.user.password=123
2、自定义表单登陆页
虽然spring security 自带的表单登陆页可以方便快速地启动,但大多数应用程序更希望提供自己的的表单登陆页,此时就需要自定义表单登陆页。
WebSecurityConfig
packagecom.oy;importorg.springframework.security.config.annotation.web.builders.HttpSecurity;importorg.springframework.security.config.annotation.web.configuration.EnableWebSecurity;importorg.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@EnableWebSecuritypublic class WebSecurityConfig extendsWebSecurityConfigurerAdapter {
@Overrideprotected void configure(HttpSecurity http) throwsException {
http.authorizeRequests().anyRequest().authenticated()
.and().formLogin().loginPage("/mylogin.html")
.loginProcessingUrl("/login") //指定处理登陆请求的路径
.permitAll() // 登陆页和 "/login" 不设置权限
.and().csrf().disable();
}
}
表单登陆页
Insert title here自定义表单登陆页
用户名:密 码:
View Code
启动项目,访问 localhost:8089/BootDemo/index/test1,自动跳转到登陆页(浏览器地址为 http://localhost:8089/BootDemo/mylogin.html)。
输入test/123, 登陆成功,拿到响应结果:
如果输入错误的用户名或密码,响应结果(状态码 302,重定向到登陆页)
对现在前后端分离的项目而言,重定向不在需要后端做,后端一般返回 json 数据,告知前端登陆成功与否,由前端决定如何处理后续逻辑,而非由服务器主动执行页面跳转。这在 Spring Security 中同样可以实现。
@EnableWebSecuritypublic class WebSecurityConfig extendsWebSecurityConfigurerAdapter {
@Overrideprotected void configure(HttpSecurity http) throwsException {
http.authorizeRequests().anyRequest().authenticated().and()
.formLogin().loginPage("/mylogin.html")
.loginProcessingUrl("/login") //指定处理登陆请求的路径//指定登陆成功时的处理逻辑
.successHandler(newAuthenticationSuccessHandler() {
@Overridepublic voidonAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication)throwsIOException, ServletException {
response.setContentType("application/json;charset=utf-8");
response.getWriter().write("{\"code\":0, \"data\":{}}");
}
})//指定登陆失败时的处理逻辑
.failureHandler(newAuthenticationFailureHandler() {
@Overridepublic voidonAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception)throwsIOException, ServletException {
response.setContentType("application/json;charset=utf-8");
response.setStatus(401);
response.getWriter().write("{\"code\":0, \"msg\":\"用户名或密码错误\"}");
}
})
.permitAll().and()
.csrf().disable();
}
}
其中,successHandler()方法带有一个 Authentication 参数,携带当前登陆用户名及其角色等信息;而 failureHandler() 方法携带一个AuthenticationException 异常参数。
3、自定义数据库模型的认证和授权
前面沿用了 Spring Security 默认的安全机制:仅有一个用户,仅有一种角色。在实际开发中,这自然是无法满足要求的。
编写三个 controller 进行测试,其中 /admin/api 下的内容是系统后台管理相关的API,必须拥有管理员权限(具有 "admin" 角色)才能访问; /user/api 必须在用户登陆并且具有 “user” 角色才能访问。
@RestController
@RequestMapping("/admin/api")public classAdminController {
@GetMapping("/hello")publicString hello() {return "hello, admin";
}
}
@RestController
@RequestMapping("/user/api")public classUserController {
@GetMapping("/hello")publicString hello() {return "hello, user";
}
}
@RestController
@RequestMapping("/app/api")public classAppController {
@GetMapping("/hello")publicString hello() {return "hello, app";
}
}
View Code
启动项目,访问 http://localhost:8089/BootDemo/user/api/hello,跳转到登陆页面,使用 test/123 登陆后。再次访问 http://localhost:8089/BootDemo/user/api/hello,此时服务器返回 403,表示用户授权失败(401 代表用户认证失败)。
3.1、使用 mysql 创建数据库
create database security_test charset=utf8;
use security_test;
create table user (
`id` bigint notnullauto_increment,
`username` varchar(100) not null,
`password` varchar(100) not null,
`enable` tinyint notnull default 1 comment '用户是否可用,1:可用,2:禁用',
`roles` varchar(500) comment '角色,多个角色用逗号隔开',
primary key (`id`),
key username (`username`)
);
insert into user(username,password,roles) values('admin','123','ROLE_user,ROLE_admin');
insert into user(username,password,roles) values('user','123','ROLE_user');
3.2、mybatis generator 生成代码
新建一个普通 Java Project
Generator 类
packagecom.oy;importjava.io.File;importjava.util.ArrayList;importjava.util.List;importorg.mybatis.generator.api.MyBatisGenerator;importorg.mybatis.generator.config.Configuration;importorg.mybatis.generator.config.xml.ConfigurationParser;importorg.mybatis.generator.internal.DefaultShellCallback;public classGenerator {public static void main(String[] args) throwsException {
List warnings = new ArrayList();boolean overwrite = true;
File configFile= new File("src/com/oy/generator.xml");
ConfigurationParser cp= newConfigurationParser(warnings);
Configuration config=cp.parseConfiguration(configFile);
DefaultShellCallback callback= newDefaultShellCallback(overwrite);
MyBatisGenerator myBatisGenerator= newMyBatisGenerator(config, callback, warnings);
myBatisGenerator.generate(null);
}
}
View Code
generator.xml
/p>
PUBLIC"-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
View Code
运行 Generator#main(),即可生成代码。
上面的 jar 可以从 maven 仓库下载(建个 maven 工程,jar包下载到本地仓库,手动复制到上面的项目中)
org.mybatis
mybatis-spring
1.3.0
org.mybatis
mybatis
3.2.6
mysql
mysql-connector-java
5.1.36
com.qiukeke
mybatis-generator-limit-plugin
1.0.4
View Code
mybatis-generator-limit-plugin-1.0.4.jar 是个 mybatis 分页插件,会在 实体 example 类中添加 limit、offset 两个字段(同时 mapping.xml 文件中也加入了分页功能)
3.3、springboot 整合 mybatis
依赖:
org.mybatis.spring.boot
mybatis-spring-boot-starter
1.3.2
mysql
mysql-connector-java
5.1.36
View Code
配置:
#datasource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/security_test?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=123456spring.datasource.tomcat.min-idle=5##################### MyBatis相关配置 [start] #####################
#MyBatis映射文件
mybatis.mapper-locations=classpath:com/oy/mapping/*.xml
#扫描生成实体的别名,需要和注解@Alias联合使用
mybatis.type-aliases-package=com.oy.model
#MyBatis配置文件,当你的配置比较复杂的时候,可 以使用
#mybatis.config-location=
#级联延迟加载。true:开启延迟加载
mybatis.configuration.lazy-loading-enabled=true
#积极的懒加载。false:按需加载
mybatis.configuration.aggressive-lazy-loading=false
##################### MyBatis相关配置 [end] ######################
View Code
在主 springboot 配置类上添加注解 @MapperScan 扫描 dao 接口生成代理对象
@SpringBootApplication
@MapperScan("com.oy.dao")public classApplication {public static voidmain(String[] args) {
SpringApplication.run(Application.class, args);
}
}
View Code
写测试代码,进行测试:
@RestController
@RequestMapping("/app/api")public classAppController {
@AutowiredprivateUserService userService;
@RequestMapping("/{id}")publicString findById(@PathVariable Long id) {
User dbUser=userService.getUserById(id);returnJSONObject.toJSONString(dbUser);
}
@GetMapping("/hello")publicString hello() {return "hello, app";
}
}
@Servicepublic class UserServiceImpl implementsUserService {
@AutowiredprivateUserDao userDao;
@OverridepublicUser getUserById(Long id) {returnuserDao.selectByPrimaryKey(id);
}
}
View Code
访问 http://localhost:8089/BootDemo/app/api/1,结果:
3.4、实现 UserDetails
Spring Security 中,使用 UserDetails 来封装用户信息,包含一系列在验证时要用到的信息,比如用户名、密码、权限及其他信息,Spring Security 会根据这些信息来校验。
UserDetails 有这样一些方法:
public interface UserDetails extendsSerializable {
/*** Returns the authorities granted to the user. Cannot return null
.
*
*@returnthe authorities, sorted by natural key (never null
)*/Collection extends GrantedAuthority>getAuthorities();/*** Returns the password used to authenticate the user.
*
*@returnthe password*/String getPassword();/*** Returns the username used to authenticate the user. Cannot return null
.
*
*@returnthe username (never null
)*/String getUsername();/*** Indicates whether the user's account has expired. An expired account cannot be
* authenticated.
*
*@returntrue
if the user's account is valid (ie non-expired),
* false
if no longer valid (ie expired)*/
booleanisAccountNonExpired();/*** Indicates whether the user is locked or unlocked. A locked user cannot be
* authenticated.
*
*@returntrue
if the user is not locked, false
otherwise*/
booleanisAccountNonLocked();/*** Indicates whether the user's credentials (password) has expired. Expired
* credentials prevent authentication.
*
*@returntrue
if the user's credentials are valid (ie non-expired),
* false
if no longer valid (ie expired)*/
booleanisCredentialsNonExpired();/*** Indicates whether the user is enabled or disabled. A disabled user cannot be
* authenticated.
*
*@returntrue
if the user is enabled, false
otherwise*/
booleanisEnabled();
}
为了程序的可维护性,我没有修改 mybatis generator 根据数据库 user 表映射生成的 User 类,而是写一个新类继承 User 类,并实现 UserDetails 接口。
packagecom.oy.security;importjava.util.Collection;importjava.util.List;importorg.springframework.security.core.GrantedAuthority;importorg.springframework.security.core.userdetails.UserDetails;importcom.oy.model.User;public class SecurityUser extends User implementsUserDetails {private static final long serialVersionUID = 1L;private Listauthorities;public void setAuthorities(Listauthorities) {this.authorities =authorities;
}/*** getAuthorities() 方法本身对应的是 roles 字段,但由于结构不一样,
* 所以此类中添加一个 authorities 字段,后面自己手动设置*/@Overridepublic Collection extends GrantedAuthority>getAuthorities() {return this.authorities;
}
@Overridepublic booleanisAccountNonExpired() {return true;
}
@Overridepublic booleanisAccountNonLocked() {return true;
}
@Overridepublic booleanisCredentialsNonExpired() {return true;
}
@Overridepublic booleanisEnabled() {return true;
}
@OverridepublicString toString() {return "SecurityUser [id=" + getId() + ", username=" + getUsername() + ", password="
+ getPassword() + ", enable=" + getEnable() + ", roles=" + getRoles() + "]";
}
}
View Code
3.5、实现 UserDetailsService
UserDetailsService 仅定义了一个 loadUserByUsername() 方法,用于获取一个 UserDetails 对象。UserDetails 对象包含一系列在验证时会用到的信息,包括用户名、密码、权限等。
packagecom.oy.security;importjava.util.List;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.security.core.authority.AuthorityUtils;importorg.springframework.security.core.userdetails.UserDetails;importorg.springframework.security.core.userdetails.UserDetailsService;importorg.springframework.security.core.userdetails.UsernameNotFoundException;importorg.springframework.stereotype.Service;importcom.oy.dao.UserDao;importcom.oy.model.User;importcom.oy.model.UserExample;/***@authoroy
*@version1.0
* @date 2020年4月14日
* @time 上午10:25:02*/@Servicepublic class MyUserDetailsService implementsUserDetailsService {
@AutowiredprivateUserDao userDao;
@Overridepublic UserDetails loadUserByUsername(String username) throwsUsernameNotFoundException {//从数据库尝试获取该用户
UserExample example = newUserExample();
UserExample.Criteria criteria=example.createCriteria();
criteria.andUsernameEqualTo(username);
List userList =userDao.selectByExample(example);if (userList == null || userList.size() == 0) {throw new RuntimeException("该用户不存在");
}
SecurityUser sUser= getUser(userList.get(0));
System.out.println("sUser: " +sUser);//将数据库 roles 字段解析成 UserDetails 的权限集
sUser.setAuthorities(AuthorityUtils.commaSeparatedStringToAuthorityList(sUser.getRoles()));returnsUser;
}/*** 读取 User 对象的属性,封装一个 SecurityUser 对象
*@paramuser
*@return
*/
privateSecurityUser getUser(User user) {if (user == null) {throw new RuntimeException("该用户不存在");
}
SecurityUser sUser= newSecurityUser();
sUser.setEnable(user.getEnable());
sUser.setId(user.getId());
sUser.setPassword(user.getPassword());
sUser.setRoles(user.getRoles());
sUser.setUsername(user.getUsername());returnsUser;
}
}
View Code
3.6、其他
1)测试时报 java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null" 这个错误。原因是5.x 版本后默认开启了委派密码编码器,所以本文暂时将密码编码器设置为 noOpPasswordEncoder。
@BeanpublicPasswordEncoder passwordEncoder() {returnNoOpPasswordEncoder.getInstance();
}
View Code
PasswordEncoder 接口有两个方法
public interfacePasswordEncoder {
String encode(CharSequence var1);booleanmatches(CharSequence var1, String var2);
}
View Code
实际开发中,可以使用
@Bean
PasswordEncoder passowrdEncoder() {return newBCryptPasswordEncoder();
}
View Code
所以,当用户注册,保存用户的密码时,从 Spring 容器中获取 PasswordEncoder 实例,调用 PasswordEncoder 实例的 encode() 方法对密码进行加密(数据库存的是加密后的密码)。
2)UserDetails 接口中包含的一些方法,比如 isEnabled() 可以用来校验用户状态(是否删除),isAccountNonLocked() 可以用来校验用户状态(是否冻结)等。可以根据业务场景进行实现,比如:
@Overridepublic booleanisEnabled() {if (getEnable().intValue() == 2) {return false;
}return true;
}
View Code
===================================================================================================
至此,代码写完了。当使用 admin/123 登陆后,再次访问 http://localhost:8089/BootDemo/admin/api/1, 就不会返回 403 了。
总结一下认证和授权过程:
1) 用户使用 admin/123 登陆时,Spring Security 调用 UserDetailsService#loadUserByUsername() 读取数据库,查出是否有 admin 这个用户名,有则读取,并将用户名、密码、权限封装成一个 UserDetails 对象返回。然后,Spring Security 根据UserDetails 对象的密码与表单传来的密码比较。
2) 当访问非公开权限的资源时,调用UserDetails#getAuthorities() 进行权限校验。
本文内容包括:
处理用户信息获取逻辑 UserDetailsService
处理用户校验逻辑 UserDetails
处理密码加密解密 PasswordEncoder
自定义登陆页面
自定义登陆成功处理 AuthenticationSuccessHandler
自定义登陆失败处理 AuthenticationFailureHandler
参考:
1)《Spring Security 实战》-- 陈木鑫