放心学,很安全
Spring Security可以说是Java Web开发必须要学的技能 了,就像我每天早上起来都要望一望镜子中帅气的我(yue~),好啦,废话不多说,一起来学习一下Spring Security吧,冲冲冲
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLicmbw5iZkZjZwMzNwU2MkJmYyIzMwATM2ITYhNjY4kzN1cTY38CX0JXZ252bj91Ztl2Lc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
1. 什么是 Spring Security ?
Spring Security是基于spring的轻量级安全框架,与spring mvc有很好的继承,提供了全面的安全性解决方案,同时在web请求级和方法调用级处理身份确认和授权。在spring的基础上,security充分利用了依赖注入和面向切面基础,为应用系统提供了声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。
1.1 功能
- 认证(你是谁,用户/设备/系统)
- 验证(你能干嘛,也叫权限控制/授权,允许执行的操作)
1.2 原理
基于Filter,Servlet,AOP实现身份认证和权限验证
1.3 管中窥豹
下面我们就用一个小栗子来先体验一下吧~
- 我们先创建一个空的工程和maven模块
- 然后加入依赖
<!--加入sb-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-parent</artifactId>
<version>2.0.6.RELEASE</version>
</parent>
<!--指定依赖-->
<dependencies>
<!--web开发相关依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--security-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
- 创建启动类
@SpringBootApplication
public class FirstApplication {
public static void main(String[] args) {
SpringApplication.run(FirstApplication.class,args);
}
}
- 创建controller,接收请求
@RestController
@RequestMapping("/hello")
public class HelloSecurityController {
@RequestMapping("/world")
public String sayHello(){
return "spring security 安全框架";
}
}
- 启动项目,可以看到启动日志出现一个uuid密码。这是security的默认临时密码,每次都不一样
“安全”地学习Spring Security放心学,很安全 - 访问项目,会出现默认的登录界面,我们输入默认用户名user,然后随便输入密码,可以发现报错;我们把密码换成默认的密码,则登录成功
“安全”地学习Spring Security放心学,很安全 “安全”地学习Spring Security放心学,很安全
1.4 自定义
从上面看到security会有默认的用户名和密码,当然我们也可以在application.yml配置文件中进行自定义
spring:
security:
user:
name: root
password: root
1.5 关闭验证功能
要关闭验证功能,只需将启动类的启动注解改成
@SpringBootApplication(exclude = {SecurityAutoConfiguration.class})
,即可
2. 基于内存用户信息
我们不可能都在配置文件中设置用户名和密码,这样的操作量太多了;这里我们可以继承
WebSecurityConfigurerAdapter
,重写方法
protected void configure(AuthenticationManagerBuilder auth)
,实现自定义认证信息
- 创建
配置类config.MyWebSecurityConfig
@Configuration//标识当前是一个配置类,这个类方法的返回值是java对象,这些对象放入到spring容器中
@EnableWebSecurity//开启security
public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {//继承WebSecurityConfigurerAdapter
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {//在重写方法中配置用户和密码信息,作为登录的数据
//下面的密码要调用密码加密类进行加密,不然会报错(security5后)
PasswordEncoder pe = passwordEncoder();
//例如在内存中预先保存两个用户登录信息
auth.inMemoryAuthentication()
.withUser("aa")
.password(pe.encode("654321"))
.roles();
auth.inMemoryAuthentication()
.withUser("bb")
.password(pe.encode("123456"))
.roles();
auth.inMemoryAuthentication()
.withUser("cc")
.password(pe.encode("123456"))
.roles();
}
@Bean//把方法的返回值对象,放到spring的容器中
public PasswordEncoder passwordEncoder(){//创建密码加密类
//创建PasswordEncoder的实现类,实现类是加密算法,有很多种加密算法实现类
return new BCryptPasswordEncoder();
}
}
这样所设置的用户就可以用对应的密码进行登录啦
2.1 角色设置
基于角色role的身份认证同一个用户可以有不同的角色,同时可以开启对方法级别的认证
- 配置
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)//启动方法界别的认证,默认是false,true表可以使用@PreAuthorize和@ProAuthorize
public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
PasswordEncoder pe = passwordEncoder();
//定义两个角色 normal,admin
auth.inMemoryAuthentication()
.withUser("aa")
.password(pe.encode("654321"))
.roles("normal");
auth.inMemoryAuthentication()
.withUser("bb")
.password(pe.encode("123456"))
.roles("normal");
auth.inMemoryAuthentication()
.withUser("cc")
.password(pe.encode("123456"))
.roles("normal","admin");//注意角色不同
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
-
方法设置
在对应方法上添加注解
@PreAuthorize
@RequestMapping("/helloUser")
@PreAuthorize(value = "hasAnyRole('admin','normal')")//注意要单引号
public String helloCommonUser(){//normal和admin都可访问
return "hello 拥有normal,admin角色的用户";
}
@RequestMapping("/helloAdmin")
@PreAuthorize(value = "hasAnyRole('admin')")
public String helloAdmin(){//指定admin访问
return "hello 拥有admin角色的用户";
}
-
启动,访问
我们用普通用户bb访问admin专属资源
可以看到无法访问,接着我们用有双重身份的cc区访问admin资源,则会发现可以访问“安全”地学习Spring Security放心学,很安全
2.1.1 设置用户角色访问步骤
- 继承
,重写WebSecurityConfigurerAdapter
方法,指定用户rolesconfigure()
- 在类上加入启动方法级别的注解
- 在处理器方法上加入角色信息,指定方法可以访问的角色列表
3. 基于JDBC的用户认证
对于小型,自己玩的项目可以用上面的基于内存的权限管理,但是大项目,大用户量用这种就是脑子瓦特了,所以接下来我们要介绍基于jdbc的用户认证,从数据库mysql中获取用户身份信息(用户名,密码和角色)
security 对象用户信息的表示类是
UserDetails
,
UserDetails
是一个接口,高度抽象的用户信息类(相当于项目中的User类);需要向security提供
User
对象,这个对象的数据来自数据库查询
我们可以在
UserDetails
中 按
ctrl+h
查看当前的实现类,
User
类 是
UserDetails
接口的实现类,构造方法有三个参数,
username
,
password
和
authorities
;
3.1 实现步骤
-
创建项目
创建新module,选择webapp模板,在main目录下创建java和resources目录和指定。依赖直接copy前面的依赖情况,再加上jdbc和jpa的依赖
“安全”地学习Spring Security放心学,很安全
<!-- mysql驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.9</version>
</dependency>
<!-- 数据库访问框架-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
- 创建dao和service Enrity
“安全”地学习Spring Security放心学,很安全
//标识当前类是实体类,表示数据库中的一个表,表名默认和类名一致
@Entity
public class UserInfo {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String password;
//角色
private String role;
//省略get/set方法
}
Dao
public interface UserInfoDao extends JpaRepository<UserInfo,Long> {
//按username查询数据库信息
UserInfo findByUsername(String username);
}
Service
public interface UserInfoService {
UserInfo findUserInfo(String username);
}
@Service
public class UserInfoServiceImpl implements UserInfoService {
@Autowired
private UserInfoDao dao;
@Override
public UserInfo findUserInfo(String username) {
UserInfo userInfo = dao.findByUsername(username);
return userInfo;
}
}
-
创建配置文件和启动类
application.properties
spring.datasource.url=jdbc:mysql://localhost:3306/sb-jpa?serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.initialization-mode=always
#springboot2.x以上需要加这一句配置,不然无法执行sql和建表
spring.jpa.generate-ddl=true
spring.jpa.show-sql=true
spring.jpa.database=mysql
启动类
略
启动,可以看到数据库创建了相应的表
4. 初始化用户信息
现在表格还是空的,我们创建一个初始化类,在当中添加一些用户信息
创建init.JdbcInit
@Component
public class JdbcInit {
@Autowired
private UserInfoDao dao;
@PostConstruct
public void init(){
PasswordEncoder pe = new BCryptPasswordEncoder();
UserInfo u = new UserInfo();
u.setUsername("tiao");
u.setPassword(pe.encode("123456"));
u.setRole("normal");
dao.save(u);
u = new UserInfo();
u.setUsername("admin");
u.setPassword(pe.encode("admin"));
u.setRole("admin");
dao.save(u);
}
}
启动后可以看到表格新增了用户信息,密码是加密后的内容
- 实现
接口,重写UserDetailsService
UserDetails loadUserByUsername(String vat1)
,在方法中获取数据库的用户信息,即执行数据库查询,条件是用户名称
provider.MyUserDetailService
@Component("MyUserDetailService")//别名
public class MyUserDetailService implements UserDetailsService {
@Autowired
private UserInfoDao dao;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = null;
UserInfo userInfo = null;
if(username != null){
userInfo = dao.findByUsername(username);
if(userInfo != null){
List<GrantedAuthority> list = new ArrayList<>();
//角色必须以ROLE_开头
GrantedAuthority authority = new SimpleGrantedAuthority("ROLE_"+userInfo.getRole());
list.add(authority);
//创建User对象
user = new User(userInfo.getUsername(),userInfo.getPassword(),list);
}
}
return user;
}
}
- 创建配置类(copy前面的就好~)
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
@Qualifier("MyUserDetailService")//找到对应别名
private UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//指定加密类型
auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());
}
}
-
创建控制器的方法(还是copy~~)
略
-
启动项目
记得把init的
和@Component
注释掉,不然启动项目继续添加用户信息@PostConstruct
-
效果展示
略
3.2 RBAC
RBAC是基于角色的访问控制(Role-Based Access Control);在RBAC中,权限和角色相关联,用户通过称为适当的角色成员而得到这些角色的权限。这极大地简化了权限的管理。这样管理都是层级相互依赖的,权限赋予给角色,而角色又赋予用户,这样的权限设计清楚,管理方便
用户是属于角色的,角色拥有权限的集合。用户属于某个角色,他就有角色对应的权限
3.2.1 RBAC表的设计
一般RBAC中表的设计可以分为以下
- 用户表:用户认证(登录用到的表);用户名,密码,是否启动,是否锁定
- 角色表:定义角色信息;角色名称,角色描述
- 用户和角色关系表:用户和角色是多对多的关系,一个用户可以有多个角色,一个角色可以有多个用户
- 权限表:角色有哪些权限
- 角色和权限关系表
3.3 详解认证中的接口和类
3.3.1 认证类 UserDetailsService
UserDetailsService
我们上面的一系列操作都离不开这个
UserDetailsService
认证类,下面我们就来详细看看是什么原理吧
-
:接口,表示用户信息的UserDetails
“安全”地学习Spring Security放心学,很安全
就实现了User
UserDetails
可以自定义类实现“安全”地学习Spring Security放心学,很安全
接口,作为自己系统的用户类,这个类可以交给security使用UserDetails
-
:我们需要实现UserDetailsService
方法来根据用户名称,来获取用户信息(用户名称,密码,角色集合,是否可用,是否锁定等信息)loadUserByUsername()
public interface UserDetailsService {
UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}
-
-
InMemoryUserDetailsManager
:在内存中维护用户信息
使用方便,但数据不持久,系统重启后数据恢复原样
-
-
-
:用户信息存放在数据库中,底层使用JdbcUserDetailsManager
操作数据库;可以用JdbcDaoImpl
中的方法完成用户的管理JdbcUserDetailsManager
其中有很多方法如“安全”地学习Spring Security放心学,很安全
、createUser
、updateUser
和deleteUser
userExeists
-
我们可以打开
org.springframework.security.core.userdetails.jdbc
,其中有个users.ddl定义文件
3.4 实现
-
InMemoryUserDetailsManager
- 我们先创建一个快速maven模块
- 加入依赖(sb、security和web)
- 创建应用的配置类
-
- 创建密码的处理类对象
config.ApplicationConfig
- 创建密码的处理类对象
-
- 使用
创建用户InMemoryUserDetailsManager
- 使用
@Configuration
public class ApplicationConfig {
//创建PasswordEncoder
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Bean
public UserDetailsService userDetailsService(){//创建UserDetailsService的实现类对象
PasswordEncoder encoder = passwordEncoder();
//创建内存的UserDetailsService的实现类对象
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
//创建用户
manager.createUser(User
.withUsername("admin")
.password(encoder.encode("admin"))
.roles("ADMIN","USER")
.build());
manager.createUser(User
.withUsername("tiao")
.password(encoder.encode("123"))
.roles("USER")
.build());
//。。。。
return manager;
}
}
- 创建类继承
,自定义安全配置WebSecurityConfigurerAdapter
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService detailsService = null;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.userDetailsService(detailsService);
}
}
- 创建控制器方法和启动类
- 测试
“安全”地学习Spring Security放心学,很安全
-
JdbcUserDetailsManager
-
创建表
我们前面打开了ddl文件,我们将其拷贝到SQLyog中,将其中的 varchar_ignorecase 类型改成 mysql 的 varchar类型,然后执行,可以看到新建了两个表
“安全”地学习Spring Security放心学,很安全 - 新建module:新建一个quickmodule,略
- 加入依赖:在上一部分的基础上,加上mysql和jdbc的依赖
- 创建应用的配置类,创建
对象:获取数据库中users表的数据JdbcUserDetailsService
@Configuration
public class ApplicationConfig {
@Autowired
private DataSource dataSource;
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
//创建JdbcUserDetailsService对象
@Bean(name = "jdbcUserDetailsService")
public UserDetailsService jdbcUserDetailsService(){
PasswordEncoder encoder = passwordEncoder();
//初始数据源DataSource,JdbcUserDetailsManager的构造方法是带datasource参数的
JdbcUserDetailsManager manager = new JdbcUserDetailsManager(dataSource);
manager.createUser(User.withUsername("admin")
.password(encoder.encode("admin"))
.roles("ADMIN","USER","MANAGER").build());
manager.createUser(User.withUsername("aa")
.password(encoder.encode("aa"))
.roles("USER").build());
manager.createUser(User.withUsername("bb")
.password(encoder.encode("bb"))
.roles("USER","NORMAL").build());
return manager;
}
}
- 创建一个security的配置,自定义安全配置信息,指定
类JdbcUserDetailsService
@EnableWebSecurity
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
@Qualifier("jdbcUserDetailsService")
private UserDetailsService userDetailsService;
@Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http.userDetailsService(userDetailsService);
}
}
- 修改application.properties文件:连接数据库,配置数据源DataSource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/sb-jpa
spring.datasource.username=root
spring.datasource.password=root
- 测试,略
3.4.1 实现表单登录
前面都是用security自带创建的表格,接下来我们自行创建表格来测试
- 创建用户表
“安全”地学习Spring Security放心学,很安全 - 创建角色表
“安全”地学习Spring Security放心学,很安全 - 创建用户角色关系表
“安全”地学习Spring Security放心学,很安全 - 构建快速maben项目
- 加入依赖:比前面的依赖多一个mybatis
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.0.1</version>
</dependency>
- 编写application.properties:连接数据库,创建连接池
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/sb-jpa
spring.datasource.username=root
spring.datasource.password=root
mybatis.config-location=classpath:/mapper/*Mapper.xml
mybatis.type-aliases-package=com.tiao.entity
- 创建自己的user类,代替UserDetails
public class SysUser implements UserDetails {
private int id;
private String username;
private String password;
private String realname;
private boolean isExpired;
private boolean isLocked;
private boolean idCredentials;
private boolean isEnabled;
private Date createTime;
private Date loginTime;
private List<GrantedAuthority> authorities;
public SysUser() {
}
public SysUser(int id,String username, String password, String realname, boolean isExpired,
boolean isLocked, boolean idCredentials, boolean isEnabled, Date createTime,
Date loginTime, List<GrantedAuthority> authorities) {
this.id = id;
this.username = username;
this.password = password;
this.realname = realname;
this.isExpired = isExpired;
this.isLocked = isLocked;
this.idCredentials = idCredentials;
this.isEnabled = isEnabled;
this.createTime = createTime;
this.loginTime = loginTime;
this.authorities = authorities;
}
public int getId() {
return id;
}
public boolean isExpired() {
return isExpired;
}
public Date getCreateTime() {
return createTime;
}
public Date getLoginTime() {
return loginTime;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return isExpired;
}
@Override
public boolean isAccountNonLocked() {
return isLocked;
}
@Override
public boolean isCredentialsNonExpired() {
return idCredentials;
}
@Override
public boolean isEnabled() {
return isEnabled;
}
public String getRealname() {
return realname;
}
public void setUsername(String username) {
this.username = username;
}
public void setPassword(String password) {
this.password = password;
}
public void setRealname(String realname) {
this.realname = realname;
}
public void setExpired(boolean expired) {
isExpired = expired;
}
public void setLocked(boolean locked) {
isLocked = locked;
}
public void setIdCredentials(boolean idCredentials) {
this.idCredentials = idCredentials;
}
public void setEnabled(boolean enabled) {
isEnabled = enabled;
}
public void setCreateTime(Date createTime) {
this.createTime = createTime;
}
public void setLoginTime(Date loginTime) {
this.loginTime = loginTime;
}
public void setAuthorities(List<GrantedAuthority> authorities) {
this.authorities = authorities;
}
}
//省略get/set方法
mapper.SysUserMapper
//@Repository:创建dao对象
@Repository
public interface SysUserMapper {
int insertSysUser(SysUser user);
}
mapper. SysUserMapper
//@Repository:创建dao对象
@Repository
public interface SysUserMapper {
int insertSysUser(SysUser user);
//根据账号名称,获取用户信息
SysUser selectSysUser(String username);
}
创建mapper文件
<mapper namespace="com.tiao.mapper.SysUserMapper">
<insert id="insertSysUser" >
insert into sys_user(username,password,realname,
isenable,islock,iscredentials,createtime,logintime)
values(#{username},#{password},#{realname},#{isEnabled},
#{isLocked},#{idCredentials},#{createTime},#{loginTime},)
</insert>
</mapper>
启动类
@MapperScan(value = "com.tiao.mapper")
@SpringBootApplication
public class UserRoleApplication {
public static void main(String[] args) {
SpringApplication.run(UserRoleApplication.class,args);
}
}
启动类
@MapperScan(value = "com.tiao.mapper")
//@ComponentScan(basePackages = "com.tiao.mapper")
@SpringBootApplication
public class UserRoleApplication {
@Autowired
SysUserMapper userMapper;
public static void main(String[] args) {
SpringApplication.run(UserRoleApplication.class,args);
}
@PostConstruct
public void jdbcInit(){
Date curDate = new Date();
PasswordEncoder encoder = new BCryptPasswordEncoder();
List<GrantedAuthority> l1 = new ArrayList<>();
//参数角色名称,需要以“ROLE_”开头,后面加上自定义角色名称,一般是从数据库获得,这里作为演示才这样写
GrantedAuthority authority = new SimpleGrantedAuthority("ROLE_"+"USER");
l1.add(authority);
List<GrantedAuthority> l2 = new ArrayList<>();
GrantedAuthority authority2 = new SimpleGrantedAuthority("ROLE_"+"READ");
l2.add(authority2);
List<GrantedAuthority> l3 = new ArrayList<>();
GrantedAuthority authority3 = new SimpleGrantedAuthority("ROLE_"+"ADMIN");
l3.add(authority3);
SysUser u1 = new SysUser(
"u1",encoder.encode("123456"),"用户",true,true,true,true,curDate,curDate,l1
);
SysUser u2 = new SysUser(
"u2",encoder.encode("123456"),"读",true,true,true,true,curDate,curDate,l2
);
SysUser u3 = new SysUser(
"u3",encoder.encode("123456"),"管理员",true,true,true,true,curDate,curDate,l3
);
userMapper.insertSysUser(u1);
userMapper.insertSysUser(u2);
userMapper.insertSysUser(u3);
}
}
初始化数据库信息
创建SysUser查询方法
<!-- 定义列和属性的对应关系-->
<resultMap id="userMapper" type="com.tiao.entity.SysUser">
<id column="id" property="id"/>
<result column="username" property="username"/>
<result column="password" property="password"/>
<result column="realname" property="realname"/>
<result column="isenable" property="isEnabled"/>
<result column="islock" property="isLocked"/>
<result column="iscredentials" property="isCredentials"/>
<result column="createtime" property="createTime"/>
<result column="logintime" property="loginTime"/>
<result column="isexpire" property="isExpired"/>
</resultMap>
<select id="selectSysUser" resultMap="userMapper">
select id,username,password,realname,isexpire,
isenable,islock,iscredentials,createtime,logintime
from sys_user where username=#{username}
</select>
创建SysRoleMapper和mapper文件,和查询角色信息方法
public class SysRole {
private Integer id;
private String name;
private String memo;
//省略get/set
}
@Repository
public interface SysRoleMapper {
List<SysRole> selectRoleByUser(Integer userId);
}
<mapper namespace="com.tiao.mapper.SysRoleMapper">
<!-- 定义列和属性的对应关系-->
<resultMap id="roleMapper" type="com.tiao.entity.SysRole">
<id column="id" property="id"/>
<result column="rolename" property="rolename"/>
<result column="rolememo" property="memo"/>
</resultMap>
<select id="selectRoleByUser" resultMap="roleMapper">
SELECT r.id,r.rolename,r.rolememo FROM sys_user_role ur,sys_role r
WHERE ur.roleid = r.id AND ur.userid=#{userid}
</select>
</mapper>
- 创建自定义的UserDetailsService实现类,在重写方法中,查询数据库获取用户信息,获取角色数据,构建UserDetails实现类对象
@Service
public class JdbcUserDetailsService implements UserDetailsService {
@Autowired
private SysUserMapper userMapper;
@Autowired
private SysRoleMapper roleMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//1.根据username获取SysUser
SysUser user = userMapper.selectSysUser(username);
if(user != null){
//2.根据userid获取role
List<SysRole> roleList = roleMapper.selectRoleByUser(user.getId());
List<GrantedAuthority> authorities = new ArrayList<>();
String roleName = "";
for(SysRole role : roleList){
roleName = role.getName();
GrantedAuthority authority = new SimpleGrantedAuthority("ROLE_"+roleName);
authorities.add(authority);
}
user.setAuthorities(authorities);
return user;
}
return user;
}
}
- 创建继承 WebSecurityConfigrerAdapter:自定义安全配置
@Configuration
@EnableWebSecurity
public class CustomSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
//@Qualifier(value = "jdbcUserDetailsService")
private UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// super.configure(auth);
auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http.authorizeRequests().antMatchers("/index").permitAll()
.antMatchers("/access/user/**").hasRole("USER")
.antMatchers("/access/read/**").hasRole("READ")
.antMatchers("/access/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
.and()
.formLogin();
}
}
-自定义登录
-
-
传统form登录
创建/resources/static/index.html
“安全”地学习Spring Security放心学,很安全
-
<p>验证访问</p>
<a href="/access/user">u1</a>
<a href="/access/read">u2</a>
<a href="/access/admin">u3</a>
- 创建controller
@RestController
public class MyController {
@GetMapping(value = "/access/user",produces = "text/html;charset=utf-8")
public String sayUser(){
return "u1 是 user 角色";
}
@GetMapping(value = "/access/read",produces = "text/html;charset=utf-8")
public String sayRead(){
return "u2 是 read 角色";
}
@GetMapping(value = "/access/admin",produces = "text/html;charset=utf-8")
public String sayAdmin(){
return "u3 是 admin 角色";
}
}
@Controller
public class IndexController {
@GetMapping("/index")
public String toIndexHtml(){
return "forward:/index.html";
}
}
3.4.2 总结:基于表单的实现步骤
- 创建maven项目
- 加入依赖
- 创建实体类:
实现SysUser
;UserDetails
SysRole
- 配置文件application.properties:配置数据源和mapper相关
- mapper类:定义对应方法
- mapper配置文件:定义sql
- 创建
接口的实现类:根据username从数据库查询账号信息UserDetailsService
对象,再根据user的id查询SysRole表中的Role信息SysUser
- 继承
,对权限进行配置WebSecurityConfigurerAdapter
- 创建主类:包括对security数据库信息的初始化
- 创建
,首页文件/resources/static/index.html
- 创建controller,包括重定向到首页和接收请求
自定义登录页面:security自带的登录页面是没有美化的,同时默认的登录页面的访问地址是,请求参数用户名是
/login
,密码是
username
(可以在默认的登录页面查看页面元素进行查看);如果我们需要自定义页面的化,需要进行设置;security登录实现的基础是过滤器
password
。
AbstractAuthenticationProcessingFilter
自定义过程:
- 写自定义登录页面
- 在配置文件中写入:自定义登录视图页面
,并且要指定其可以直接访问.loginPage("/a.html");
.antMatchers("a.html").permitAll()
- 设置form中登录的访问uri地址
.loginProcessingUrl("/login");
- 设置关于跨域访问的安全设置,先禁用
.csrf().disable();
- 如果需要,我们可以再加入404的页面
.failureUrl(/error.html)
- 可以通过 http 对参数进行自定义:
.usernameParameter("a").passwordParameter("b")
3.4.3 实现ajax登录
基于表单方式很少用,现在都是前后端分离,一般都是使用json作为数据交换格式
- 在前面项目的基础上改造,创建新的登录页面
static/Myajax.html
<script type="text/javascript" src="../static/js/jquery-3.1.0.js"></script>
<script type="text/javascript">
$(function () {
//jquery的入口函数
$("#btnLogin").click(function () {
alert("click");
var uname = $("#username").val();
var pwd = $("#password").val();
$.ajax({
url: "/login",
type: "POST",
data: {
"username":uname,
"password":pwd
},
dataType:"json",
success:function (resp) {
alert(resp.msg)
}
})
})
})
</script>
</head>
<body>
<p>前后端分离的ajax请求方式</p>
<div>
用户名:<input type="text" id="username" value=""><br/>
密码:<input type="text" id="password" value=""><br/>
<button id="btnLogin">使用ajax登录</button>
</div>
</body>
-
设置配置文件
api说明
-
:security验证用户信息成功后执行的接口,执行其AuthenticationSuccessHandler
方法onAuthenticationSuccess()
-
:security验证用户失败后执行的接口,执行其AuthenticationFailureHandler
onAuthenticationFailure()
方法
创建
common.MySuccessHandler
@Component
public class MySuccessHandler implements AuthenticationSuccessHandler {
/**
*
* @param request 请求对象
* @param response 应答对象
* @param authentication security验证用户信息成功后的封装类
*/
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
//登录的用户信息验证成功后执行的方法
response.setContentType("text/json;charset=utf-8");
PrintWriter pw = response.getWriter();
pw.println("{\"msg\":\"登录成功\"}");
pw.flush();
pw.close();
}
}
MyFailureHandler
类似,这里省略
设置配置文件
@Configuration
@EnableWebSecurity
public class CustomSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
// @Qualifier(value = "jdbcUserDetailsService")
private UserDetailsService userDetailsService;
//把两个处理类注入进来
@Autowired
private AuthenticationFailureHandler failureHandler;
@Autowired
private AuthenticationSuccessHandler successHandler;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// super.configure(auth);
auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http.authorizeRequests().antMatchers("/index","/Myajax.html","/login","/js/**").permitAll()
.antMatchers("/access/user/**").hasRole("USER")
.antMatchers("/access/read/**").hasRole("READ")
.antMatchers("/access/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
.and()
.formLogin()
.successHandler(successHandler)
.failureHandler(failureHandler)
.loginPage("/Myajax.html")
.loginProcessingUrl("/login")
.and()
.csrf();
}
}
-
测试,启动项目,访问index,输入账号密码
输入错误时
输入正确时“安全”地学习Spring Security放心学,很安全 “安全”地学习Spring Security放心学,很安全 - 使用jackson处理json
- 加入依赖
<!-- jackson-->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.9.8</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.8</version>
</dependency>
- 创建消息结果
vo.Result
public class Result {
//code=0成功,code=1失败
private int code;
//表示错误代码
private int error;
//消息文本
private String msg;
//省略get/set
}
- 修改
,MySuccessHandler
同理MyFailureHandler
response.setContentType("text/json;charset=utf-8");
Result result = new Result();
result.setCode(0);
result.setError(1000);
result.setMsg("登录成功");
OutputStream out = response.getOutputStream();
ObjectMapper om = new ObjectMapper();
om.writeValue(out,result);
out.flush();
out.close();
- 修改ajax的结果
- 结果测试
“安全”地学习Spring Security放心学,很安全 “安全”地学习Spring Security放心学,很安全
3.4.4 实现ajax登录实现总结
- 在登录页面中加入jQuery
- 在登录页面中加入Ajax
- 在登录页面中加入dom对象
- 创建handler,实现两个不同的接口,成功和失败
- 加入jackson依赖
- 创建作为结果的对象Result
- 配置handler
3.5 生成验证码
生成验证码可以通过自定义实现,也可以通过开源库来实现;验证码的实现可以基于servlet,也可以使用controller;这里我们通过自定义和controller来实现验证码
- 创建
CaptchaController
@Controller
@RequestMapping("/captcha")
public class CaptchaController {
//定义值,用于生成验证码图片
private int width = 120;
private int height = 30;
//图片内容在图片的起始点
private int drawY = 20;
//文字间隔
private int space = 15;
//文字个数
private int charCount = 4;
//验证码内容数组
private String chars [] = {"A","B","C","D","E","1","2","3","4","5"};
//定义方法:生成验证码内容,在图片上写文字
@GetMapping("/code")
public void makeCaptchaCode(HttpServletRequest request,HttpServletResponse response){
/**
* 验证码:需要在内存中绘制一个图片BufferedImage
* 向这个图片写入文字,把绘制好的内容的图片响应给请求
*/
//创建一个透明背景,rgb表颜色
BufferedImage image = new BufferedImage(width,height,BufferedImage.TYPE_INT_RGB);
//获取画笔
Graphics g = image.getGraphics();
//设置画笔是白色的
g.setColor(Color.white);
//把画板涂白
//fillRect(矩形起始x,矩形起始y,矩形宽,矩形高)
g.fillRect(0,0,width,height);
//创建字体
Font f = new Font("宋体",Font.BOLD,16);
g.setFont(f);
/**
* 在画布上写字
* 参数:文字,x,xy坐标
* g.drawString("字",10,drawY)
*/
StringBuffer buffer = new StringBuffer("");
int ran = 0;
int len = chars.length;
for(int i=0;i<charCount;i++){
ran = new Random().nextInt(len);
//获取要写上去的内容
buffer.append(chars[ran]);
//设置颜色
g.setColor(makeColor());
g.drawString(chars[ran],(i+1)*space,drawY);
}
//绘制干扰线
for(int m=0;m<4;m++){
g.setColor(makeColor());
int dot[] = makeLineDot();
g.drawLine(dot[0],dot[1],dot[2],dot[3]);
}
//把生成的验证码存储到session中
request.getSession().setAttribute("code",buffer.toString());
//设置没有缓冲
response.setHeader("Pragma","no-cache");
response.setHeader("Cache-Control","no-cache");
response.setDateHeader("Expires",0);
response.setContentType("image/png");
OutputStream out = response.getOutputStream();
//参数:输出的图像,图像格式png,jpg,jpeg
ImageIO.write(image,"png",out);
out.flush();
out.close();
}
private Color makeColor(){
Random random = new Random();
int r = random.nextInt(255);
int g = random.nextInt(255);
int b = random.nextInt(255);
return new Color(r,g,b);
}
private int[] makeLineDot(){
Random random = new Random();
int x1 = random.nextInt(width/2);
int y1 = random.nextInt(height);
int x2 = random.nextInt(width/2);
int y2 = random.nextInt(height);
return new int[]{x1,y1,x2,y2};
}
}
- 在配置文件中放行
“安全”地学习Spring Security放心学,很安全 - 修改前端代码
- 增加验证码部分
<script type="text/javascript" src="../static/js/jquery-3.1.0.js"></script>
<script type="text/javascript">
$(function () {
//jquery的入口函数
$("#btnLogin").click(function () {
alert("click");
var uname = $("#username").val();
var pwd = $("#password").val();
//增加验证码参数
var txtcode = $("#txtcode").val();
$.ajax({
url: "/login",
type: "POST",
data: {
"username":uname,
"password":pwd,
"code":txtcode
},
dataType:"json",
success:function (resp) {
alert("代码:"+resp.code + "提示:"+resp.msg)
}
})
})
})
//增加changeCode函数
function changeCode() {
//new Date的目的是让浏览器不使用缓存,每次获取新的内容
var url="/captcha/code?t="+new Date();
//使用jQuery的attr函数,设置img标签的src属性
$("#imagecode").attr("src",url);
}
</script>
</head>
<body>
<p>前后端分离的ajax请求方式</p>
<div>
用户名:<input type="text" id="username" value=""><br/>
密码:<input type="text" id="password" value=""><br/>
验证码:<input type="text" id="txtcode" value=""><br/>
<!-- 图像,显示验证码的值-->
<img id="imagecode" src="/captcha/code"/>
<a href="javascript:void(0)" onclick="changeCode">重新获取</a>
<br/>
<br/>
<button id="btnLogin">使用ajax登录</button>
</div>
</body>
前端效果
- 后端验证码验证
** 过滤器**介绍:整个security都是通过过滤器实现的,用户发起请求,通过多个过滤器才能访问到目标资源;目前用表单登录,验证用户名和密码使用的过滤器就是思路:我们要在验证username和password之前,先验证验证码的正确与否,即在
UsernamePasswordAuthenticationFilter
UsernamePasswordAuthenticationFilter
之前增加一个自定义过滤器,来判断session中的code和请求中的code是否一致,验证失败则抛出异常,security根据异常觉得身份验证是否正确
自定义过滤器方式:1.直接实现 Filter 接口 2.继承
(一个只执行一次的过滤器)
OncePerRequestFilter
- 创建过滤器异常处理类
VerificationException
public class VerificationException extends AuthenticationException {
public VerificationException(String msg, Throwable t) {
super(msg, t);
}
public VerificationException(String msg) {
super(msg);
}
public VerificationException() {
super("验证码错误,请重新输入");
}
}
- 创建过滤器
VerificationCodeFilter
public class VerificationCodeFilter extends OncePerRequestFilter {
private MyFailureHandler failureHandler = new MyFailureHandler();
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
//只有是login操作,才需要这个过滤器参与验证码的使用
String uri = request.getRequestURI();
if(!"/login".equals(uri)){
//过滤器正常执行,不参与验证码操作
filterChain.doFilter(request,response);
}else{
try{
//登录操作,需要验证code
verificationCode(request);
//如果验证通过,过滤器正常执行
filterChain.doFilter(request,response);
}catch(VerificationException e){
Result result = new Result();
result.setCode(1);
result.setError(1002);
result.setMsg("验证码错误!!");
failureHandler.onAuthenticationFailure(request,response,e);
}
}
}
private void verificationCode(HttpServletRequest request){
HttpSession session = request.getSession();
//获取请求中的code
String requestCode = request.getParameter("code");
//获取session中的code
String sessionCode = "";
Object attr = session.getAttribute("code");
if(attr != null){
sessionCode = (String) attr;
}
//处理逻辑
if(!StringUtils.isEmpty(sessionCode)){
//在session中的code,用户看到这个code了
//如果能到这段代码,说明用户已经发起登录请求了
//session中的现在的这个code就应该无效
session.removeAttribute("code");
}
//判断code是否正确
if(StringUtils.isEmpty(requestCode) ||
StringUtils.isEmpty(sessionCode) ||
!requestCode.equals(sessionCode)){
//失败
throw new VerificationException();
}
}
}
- 修改异常处理类
MyFailureHandler
“安全”地学习Spring Security放心学,很安全 “安全”地学习Spring Security放心学,很安全 - 在框架的过滤器链条中增加一个自定义过滤器
*完结撒花~~~~~*
大家可以来关注我的公众号“刘小条”,一起学习,一起感受生活~~
本学习笔记学习自王鹤老师,是本人自己的总结和理解,如有不足,请多指教