标题企业级基于springboot+mybatis+springsecurity用户授权登录
最近自己入门一下springsecurity的简单知识,自己就想着写一个简单的用户登录、拥有不同权限的人所能访问的资源是不同滴,但是在写之前访问了很多博主的博客和教学视频与我的想法都大相径庭,并不是我所想要的,用户是存储在数据库中里面,并不是在配置文件中写死的那种,这样就较为符合实际情况。
使用的主要知识
- spring boot 2.**
- spring security 5.**
- mybatis 及其逆向工程
- springmvc
- mysql
- log4J的设置(顺带的了解一些)
一、新建三个数据库表(user、role、user_role)
create database `security`;
use `security`;
create table `user`(
`id` int(11) not null auto_increment,
`username` varchar(64) default null,
`nickname` varchar(64) default null,
`password` varchar(255) default null,
primary key (`id`)
)
insert into `user` values ('1','admin','庞统','admin');
insert into `user` values (NULL,'sale','李白','sale');
insert into `user` values (NULL,'superAdmin','刘备','super');
insert into `user` values (NULL,'person','白居易','person');
insert into `user` values (NULL,'test','苏轼','test');
create table `role` (
`id` int(11) not null auto_increment,
`authority` varchar(64) default null,
primary key (`id`)
)
insert into `role` values ('1','管理员');
insert into `role` values (null, '销售员');
insert into `role` values (null, '超级管理员');
insert into `role` values (null, '普通用户');
insert into `role` values (null, '测试员');
create table `role_user` (
`id` int(11) not null auto_increment,
`uid` int(11) default '4',
`rid` int(11) default null,
primary key (`id`),
key `rid` (`rid`),
key `roles_user_ibfk_2` (`uid`),
constraint `roles_user_ibfk_1` foreign key (`rid`) references `role` (`id`),
constraint `roles_user_ibfk_2` foreign key (`uid`) references `user` (`id`) on delete cascade
)
insert into `role_user` values ('1','1','1');
insert into `role_user` values ('2','2','2');
insert into `role_user` values ('3','3','3');
insert into `role_user` values ('4','4','4');
insert into `role_user` values ('5','5','5');
二、使用IDEA创建一个Springboot项目
1. 项目目录
三、主要代码的书写
1. application.properties配置文件
主要是配置数据库、Mybatis逆向工程生成文件的存放位置以及Mybatis的相关配置、Spring MVC的HTML路径、Logging日志文件的相关配置。在这个里面有模拟用户,就是告诉内存有这样的一个人可以访问我里面的资源(这个是一种写死了的,不够灵活)。
# Mysql config
spring.datasource.url=jdbc:mysql://localhost:3306/security?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=****
spring.datasource.password=****
# 配置logger扫描的包路径,这样才会打印sql
logging.level.com.business.mapper=debug
logging.level.root=info
logging.level.org.mybatis=debug
# 在控制台上显示Controller层定义的URL
logging.level.org.springframework.web=trace
# 模拟用户
#spring.security.user.name=admin
#spring.security.user.password=admin
mybatis.mapper-locations=classpath:/mapper/*.xml
mybatis.type-aliases-package=com.business.bean
# SpringMvc Config
spring.mvc.view.prefix=/templates/
spring.mvc.view.suffix=.html
spring.freemarker.cache=false
spring.freemarker.request-context-attribute=request
#Mybatis 输出日记
# org.apache.ibatis.logging.stdout.StdOutImpl:这个使可以打印sql、参数、查询结果
# org.apache.ibatis.logging.log4j.Log4jImpl:这个不打印查询结果
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
# mybatis 逆向工程生成文件的地址
#Mybatis Generator
generator.targetProject=src/main/java
generator.targetResource=src/main/resources
generator.mappers=com.business.utils.base.IBaseMapper
generator.javaModel-targetPackage=com.business.bean
generator.sqlMap-targetPackage=mapper
generator.javaClient-targetPackage=com.business.mapper
2.MySecurityConfig 继承 WebSecurityConfigurerAdapter 类,并重写其三个config方法
- configure(HttpSecurity http) 我认为是配置Security的认证策略,每个模块的配置使用end结尾。 .authorizeRequests()配置路径拦截,就是Filter,表示的是路径对应的权限、角色、认真信息。作用就是保护请求。
- configure(WebSecurity web)配置Spring Security的Filter链。
- configure(AuthenticationManagerBuilder auth),配置user-detail服务,由于在Spring Security里面规定再登录的时候时使用密文登录的,所以在该方法里面是将用户登录时候的明文转换为密文。该方法我使用createPwdEncoder()的方法代替了。在上面所说的将用户写死了,就是这个方法中配置基于内存的用户存储(这个也是一种写死了的,不够灵活)。
package com.business.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.builders.WebSecurity;
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 org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.access.AccessDeniedHandler;
@Configuration
@EnableWebSecurity
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder createPwdEncoder(){
//在spring security5中摒弃了MD5的加密方式。将明文转换成密文。
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
/* auth.inMemoryAuthentication().withUser("user").password("123").authorities("ROLE_USER")
.and()
.withUser("admin").password("123").roles("USER","ADMIN").and()
.withUser("123").password("123").roles("ADMIN");
*/
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/idex.html","/login_page","favicon.icon","/static/**");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
//所要拦截的路径,并设置拥有什么用的权限的用户才能访问该资源
.antMatchers("/goods/get").hasRole("管理员")
.antMatchers("/goods/delete").hasRole("超级管理员")
.antMatchers("/goods/add").hasRole("销售员")
.antMatchers("/goods/change").hasRole("测试员")
.antMatchers("/login_page").permitAll()
/*要求所有进入系统的HTTP请求都要进行认证*/
.anyRequest().authenticated()
.and()
/*采用的是表单的提交方式;另外一种是httpBasic(),可以配置basic登录*/
.formLogin()
//自定义登录界面
.loginPage("/login_page")
//登录提交表单的路径,/login也是默认的登录路径
.loginProcessingUrl("/login")
.usernameParameter("username")
.passwordParameter("password")
.permitAll()
// 登录错误所跳转的页面
.failureUrl("/err")
// 登录成功所跳转的页面
.successForwardUrl("/index")
.and()
//注销
.logout().permitAll()
.and()
//禁止使用csrf()以及权限不足的时候所跳转的页面信息
.csrf().disable().exceptionHandling().accessDeniedHandler(getAccessDeniedHandler());
}
/**
* 权限不足
* @return
*/
@Bean
AccessDeniedHandler getAccessDeniedHandler(){
return new AuthenticationAccessDeniedHandler();
}
}
3.AuthenticationAccessDeniedHandler
在MySecurityConfig 有所用到,主要设置当访问的路径用户的权限不足的时候,会给予什么样的信息提醒。
package com.business.config;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
public class AuthenticationAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse resp, AccessDeniedException e) throws IOException, ServletException {
resp.setStatus(HttpServletResponse.SC_FORBIDDEN);
// resp.setCharacterEncoding("utf-8");
resp.setContentType("text/html;charset=utf-8");
PrintWriter out = resp.getWriter();
out.write("权限不足,请联系管理员!");
out.flush();
out.close();
}
}
4.User类,首先是要继承UserDetails类,并重写里面的5个方法。
package com.business.bean;
import com.fasterxml.jackson.annotation.JsonIgnore;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import javax.persistence.*;
public class User implements Serializable, UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String username;
private String nickname;
private String password;
public List<Role> getRoles() {
return roles;
}
public void setRoles(List<Role> roles) {
this.roles = roles;
}
@Transient
private List<Role> roles;
private static final long serialVersionUID = 1L;
/**
* @return id
*/
public Integer getId() {
return id;
}
/**
* @param id
*/
public void setId(Integer id) {
this.id = id;
}
/**
* @return username
*/
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
/**
* @param username
*/
public void setUsername(String username) {
this.username = username;
}
/**
* @return nickname
*/
public String getNickname() {
return nickname;
}
/**
* @param nickname
*/
public void setNickname(String nickname) {
this.nickname = nickname;
}
/**
* 获取用户所拥有的权限,合并到user用户里面
* @return
*/
@Override
@JsonIgnore
public Collection<? extends GrantedAuthority> getAuthorities() {
List<GrantedAuthority> authorities = new ArrayList<>();
for (Role role: roles){
authorities.add(new SimpleGrantedAuthority("ROLE_"+role.getAuthority()));
}
return authorities;
}
/**
* @return password
*/
public String getPassword() {
return password;
}
/**
* @param password
*/
public void setPassword(String password) {
this.password = password;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", nickname='" + nickname + '\'' +
", password='" + password + '\'' +
", roles=" + roles +
'}';
}
}
5.UserService,首先要继承UserDetailsService。
package com.business.service;
import com.business.bean.User;
import com.business.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
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.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Service
public class UserService implements UserDetailsService {
@Resource
UserMapper userMapper;
// PasswordEncoder 这个必须是在 MySecurityConfig 中有注入到Spring容器里面
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
User user = userMapper.getUsersByUserName(s);
if (user == null) {
throw new UsernameNotFoundException("没有该用户");
}
String myPassword = passwordEncoder.encode(user.getPassword());
// 这个是需要知道存储在数据库的用户的密码是明文还是密文,才考虑要不要这句话
// user.setPassword(myPassword);
return user;
}
}
为什么User和UserService要继承UserDetails和UserDetailsService呢?
答:这个就需要说一下Spring Security 是怎么验证的了。是通过AuthenticationManager接口来实现的,而这个接口有一个默认的实现类:ProviderManager,而这个类也不干实事儿。他又把验证的任务委托给:AuthenticationProvide,这个类终于时开始干实事了,AuthenticationProvide会轮流检查有前端登录的用户信息,检查完以后就会返回Authentication对象或者抛出异常。
那么AuthenticationProvide又是怎么获得用户的信息的呢 ?
这个是AuthenticationProvide有一个实现的Dao层的类DaoAuthenticationProvider,这个类依赖UserDetail和UserDetailService来获取存储在数据库或者是在配置文件中写死的用户的信息,然后将过获取到的信息存储到UserDetail的实现类中。
最后就是将已经存储了信息的UserDetail与返回的Authentication进行比较,从而判断是否通过验证,
想要查看源代码的:按Ctrl+N 搜索AuthenticationManager,在这个接口中我们可以看到这个接口最后返回的是Authentication。
.......
public interface AuthenticationManager {
........
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
}
按Ctrl+H就可以看到实现这个接口的类,在这写你就会发现ProviderManager,然后点进去看看
,在ProviderManager可以看到一个成员providers,这个成员就是存储了一个List的AuthenticationProvider,
然后再看其重写父类的方法,就会发现里面返回的result就是一个Authentication,是空的话,就会返回一个异常。
再按Ctrl+N搜索:DaoAuthenticationProvider,发现这个类的父类的父类就是AuthenticationProvider,在DaoAuthenticationProvider里面的一个成员就是UserDetailsService。看一下里面的这个代码:
protected final UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
// loadUserByUsername(s)在我们自己写的UserService就有重写这个方法的
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
catch (UsernameNotFoundException ex) {
mitigateAgainstTimingAttack(authentication);
throw ex;
}
catch (InternalAuthenticationServiceException ex) {
throw ex;
}
catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
}
}
接下来看看DaoAuthenticationProvider父类AbstractUserDetailsAuthenticationProvider中的下面这个方法:
protected Authentication createSuccessAuthentication(Object principal,
Authentication authentication, UserDetails user) {
// Ensure we return the original credentials the user supplied,
// so subsequent attempts are successful even with encoded passwords.
// Also ensure we return the original getDetails(), so that future
// authentication events after cache expiry contain the details
UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(
principal, authentication.getCredentials(),
authoritiesMapper.mapAuthorities(user.getAuthorities()));
result.setDetails(authentication.getDetails());
return result;
}
阅读了上面的代码我们发现了返回的result是new了一个UsernamePasswordAuthenticationToken,UsernamePasswordAuthenticationToken是干什么用的呢?
只看当前类我们时发现不了什么的,所以我们就查看他的父类AbstractAuthenticationToken,到了AbstractAuthenticationToken就会发现原来这个类UsernamePasswordAuthenticationToken说到底也是Authentication的继承类。
UsernamePasswordAuthenticationToken的作用:当在页面(即登陆页面)中输入用户名和密码之后会进入UsernamePasswordAuthenticationToken进行验证(Authentication),然后生成一个Authentication,并交给AuthenticationManager来进行管理,而AuthenticationManager会交给下面的若干个AuthenticationProvider干实事儿,AuthenticationProvider里面的成员Provider都会通过UserDetailsService和UserDetail以UsernamePasswordAuthenticationToken实现的带用户名和密码以及权限的Authentication。
下面就是从数据库中读取用户并进行验证
第一种:在数据库中存储的是以明文的方式
当以明文的方式的时候,在 UserService 中 loadUserByUsername方法,就会多一句user.setPassword(myPassword);(详情见上面的代码),这个为什么要加这句话呢?这是在Spring Security中验证密码的时候,必须是用密文的形式,这个较为安全。
第二种:在数据库中存储的是以密文的方式
这个就和上面贴上去的代码是一样的了。
从数据库中获得对应的用户信息 UserMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.business.mapper.UserMapper">
<resultMap id="BaseResultMap" type="com.business.bean.User">
<!--
WARNING - @mbg.generated
-->
<id column="id" jdbcType="INTEGER" property="id" />
<result column="username" jdbcType="VARCHAR" property="username" />
<result column="nickname" jdbcType="VARCHAR" property="nickname" />
<result column="password" jdbcType="VARCHAR" property="password" />
<collection property="roles" ofType="com.business.bean.Role">
<id column="id" property="id"/>
<result column="authority" property="authority"/>
</collection>
</resultMap>
<resultMap id="AddRolesResult" type="com.business.bean.User" extends="BaseResultMap">
<collection column="id" property="roles" ofType="com.business.bean.Role"
select="com.business.mapper.UserMapper.selectRolesByUid"/>
</resultMap>
<select id="getUsersByUserName" resultMap="AddRolesResult">
select user.* from user where username = #{username}
</select>
<select id="selectRolesByUid" resultType="com.business.bean.Role" parameterType="String">
select role.* from role, role_user where role.id = role_user.rid and role_user.uid = #{uid};
</select>
<update id="BCrypPassword" parameterType="String">
update user set password=#{password} where username=#{username}
</update>
</mapper>
最后,如果想实时在打印台上面看到从数据库中读出来的数据应该怎么做呢?
在application.properties配置文件中添加
#Mybatis 输出日记
# org.apache.ibatis.logging.stdout.StdOutImpl:这个使可以打印sql、参数、查询结果
# org.apache.ibatis.logging.log4j.Log4jImpl:这个不打印查询结果
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
运行结果查看
- 登陆界面
2.功能主页
3.当前使用的用户是admin,该用户的权限是管理员,所以应该是只能访问商品查询功能
做一下学习Log4J的笔记
首先了解log4J是有三个组件组成:Loggers(记录器)、Appenders(输出源)、Layouts(布局)。
Loggers
五个级别:BEBUG、INFO、WARN、ERROR、FATAL,级别是由高到低,Log4J规定
:只输出级别不低于设定级别的日志信息。
Appenders
控制日志输出到什么地方。有:控制台、文件。
常使用的类如下:
org.apache.log4j.ConsoleAppender(控制台)
org.apache.log4j.FileAppender(文件)
org.apache.log4j.DailyRollingFileAppender(每天产生一个日志文件)
org.apache.log4j.RollingFileAppender(文件大小到达指定尺寸的时候产生一个新的文件)
org.apache.log4j.WriterAppender(将日志信息以流格式发送到任意指定的地方)
Layouts
一般Layouts是跟在Appenders后面,根据用户自己的喜好格式化自己的日志输出。
常使用的类如下:
org.apache.log4j.HTMLLayout(以HTML表格形式布局)
org.apache.log4j.PatternLayout(可以灵活地指定布局模式)
org.apache.log4j.SimpleLayout(包含日志信息的级别和信息字符串)
org.apache.log4j.TTCCLayout(包含日志产生的时间、线程、类别等信息)
配置步骤
-
配置根Logger
log4j.rootLOgger=[level],appenderName1,…appenderNameN
level: 日志记录的最低级别。
appenderName:指定日志信息要输出要哪里。
eg: log4j.rootLogger=DEBUG,console,dailyFile,im
-
配日志信息输出目的地
log4j.appender.appenderName = className(上面的常使用类选择)
log4j.appender.appenderName.** = ****
- 配置日志信息输出格式
Log4J 比较全面的配置
log4j.rootLogger=DEBUG,console,dailyFile,im
log4j.additivity.org.apache=true
日志文件(logFile)
log4j.appender.logFile=org.apache.log4j.FileAppender
log4j.appender.logFile.Threshold=DEBUG
log4j.appender.logFile.ImmediateFlush=true
log4j.appender.logFile.Append=true
log4j.appender.logFile.File=D:/logs/log.log4j
log4j.appender.logFile.layout=org.apache.log4j.PatternLayout
log4j.appender.logFile.layout.ConversionPattern=[%-5p] %d(%r) --> [%t] %l: %m %x %n
回滚文件(rollingFile)
log4j.appender.rollingFile=org.apache.log4j.RollingFileAppender
log4j.appender.rollingFile.Threshold=DEBUG
log4j.appender.rollingFile.ImmediateFlush=true
log4j.appender.rollingFile.Append=true
log4j.appender.rollingFile.File=D:/logs/log.log4j
log4j.appender.rollingFile.MaxFileSize=200KB
log4j.appender.rollingFile.MaxBackupIndex=50
log4j.appender.rollingFile.layout=org.apache.log4j.PatternLayout
log4j.appender.rollingFile.layout.ConversionPattern=[%-5p] %d(%r) --> [%t] %l: %m %x %n
定期回滚日志文件(dailyFile)
log4j.appender.dailyFile=org.apache.log4j.DailyRollingFileAppender
log4j.appender.dailyFile.Threshold=DEBUG
log4j.appender.dailyFile.ImmediateFlush=true
log4j.appender.dailyFile.Append=true
log4j.appender.dailyFile.File=D:/logs/log.log4j
log4j.appender.dailyFile.DatePattern=’.'yyyy-MM-dd
log4j.appender.dailyFile.layout=org.apache.log4j.PatternLayout
log4j.appender.dailyFile.layout.ConversionPattern=[%-5p] %d(%r) --> [%t] %l: %m %x %n
应用于socket
log4j.appender.socket=org.apache.log4j.RollingFileAppender
log4j.appender.socket.RemoteHost=localhost
log4j.appender.socket.Port=5001
log4j.appender.socket.LocationInfo=true
/Set up for Log Factor 5/
log4j.appender.socket.layout=org.apache.log4j.PatternLayout
log4j.appender.socket.layout.ConversionPattern=[%-5p] %d(%r) --> [%t] %l: %m %x %n
/Log Factor 5 Appender/
log4j.appender.LF5_APPENDER=org.apache.log4j.lf5.LF5Appender
log4j.appender.LF5_APPENDER.MaxNumberOfRecords=2000
发送日志到指定邮件
log4j.appender.mail=org.apache.log4j.net.SMTPAppender
log4j.appender.mail.Threshold=FATAL
log4j.appender.mail.BufferSize=10
log4j.appender.mail.From = [email protected]
log4j.appender.mail.SMTPHost=mail.com
log4j.appender.mail.Subject=Log4J Message
log4j.appender.mail.To= [email protected]
log4j.appender.mail.layout=org.apache.log4j.PatternLayout
log4j.appender.mail.layout.ConversionPattern=[%-5p] %d(%r) --> [%t] %l: %m %x %n
应用于数据库
log4j.appender.database=org.apache.log4j.jdbc.JDBCAppender
log4j.appender.database.URL=jdbc:mysql://localhost:3306/test
log4j.appender.database.driver=com.mysql.jdbc.Driver
log4j.appender.database.user=root
log4j.appender.database.password=
log4j.appender.database.sql=INSERT INTO LOG4J (Message) VALUES(’=[%-5p] %d(%r) --> [%t] %l: %m %x %n’)
log4j.appender.database.layout=org.apache.log4j.PatternLayout
log4j.appender.database.layout.ConversionPattern=[%-5p] %d(%r) --> [%t] %l: %m %x %n
想对不同的类输出不同的文件(以com.business.User为例)
首先在User.java中加入:
然后在log4j.properties中加入:
log4j.logger.com.business.User = INFO, user
后面就是配置日志文件输出的地方、输出是什么样的类型(Console、Fier、DailyRollingFile、RollingFile),打印的方式是怎么样的(HTML、灵活等)