1. 首先构建一个springBoot+security+mybatis的项目
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.5.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.hydata</groupId>
<artifactId>springsecurity_springboot_01</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springsecurity_springboot_01</name>
<description>springSecurity01 for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.18</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
spring:
datasource:
url: jdbc:mysql://localhost:3306/security?characterEncoding=utf-8&serverTimezone=GMT%2B8
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 123
type: com.alibaba.druid.pool.DruidDataSource
mybatis:
type-aliases-package: com.hydata.springsecurity_springboot_01.entity
mapper-locations: classpath:mybatis/mapper/*Dao.xml
config-location: classpath:mybatis/mybatis.xml
@Data
public class SysUser implements Serializable , UserDetails {
private static final long serialVersionUID = 573810770728994662L;
private Integer id;
private String username;
private String realname;
private String password;
private Object createdate;
private Object lastlogintime;
private boolean enabled;
private boolean accountnonexpired;
private boolean accountnonlocked;
private boolean credentialsnonexpired;
private Collection<? extends GrantedAuthority> authorities;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.authorities;
}
@Override
public boolean isAccountNonExpired() {
return this.accountnonexpired;
}
@Override
public boolean isAccountNonLocked() {
return this.accountnonlocked;
}
@Override
public boolean isCredentialsNonExpired() {
return this.accountnonexpired;
}
@Override
public boolean isEnabled() {
return this.enabled;
}
}
@SpringBootTest
class SpringsecuritySpringboot01ApplicationTests {
@Autowired
private SysUserDao sysUserDao;
@Test
void queryById() {
SysUser sysUser = sysUserDao.queryById(1);
System.out.println(sysUser.toString());
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录页面</title>
</head>
<body>
<h3>登录页面</h3>
<form action="/login" method="post">
用户名:<input type="text" name="username"/><br/>
密 码:<input type="password" name="password"/><br/>
验证码:<input type="text" name="imageCode"><img src="/getVerifyCode"/><br/>
<input type="submit" value="登录"/>
</form>
</body>
</html>
controller层的结构:
MainController :
@Controller
public class MainController {
@RequestMapping("index")
public String index(){
return "index";
}
@RequestMapping(value = "login",method = RequestMethod.GET)
public String login(){
return "login";
}
@RequestMapping("accessDeined")
public String accessDeined(){
return "accessDeined";
}
}
ImageContrller :
@Controller
@Slf4j
public class ImageContrller {
/* 获取验证码图片*/
@RequestMapping("/getVerifyCode")
public void getVerificationCode(HttpServletResponse response, HttpServletRequest request) {
OutputStream os = null;
try {
int width = 200;
int height = 69;
BufferedImage verifyImg = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
//生成对应宽高的初始图片
String randomText = VerifyCode.drawRandomText(width, height, verifyImg);
//单独的一个类方法,出于代码复用考虑,进行了封装。
//功能是生成验证码字符并加上噪点,干扰线,返回值为验证码字符
request.getSession().setAttribute("verifyCode", randomText);
response.setContentType("image/png");//必须设置响应内容类型为图片,否则前台不识别
os = response.getOutputStream(); //获取文件输出流
ImageIO.write(verifyImg, "png", os);//输出图片流
os.flush();
} catch (IOException e) {
log.error(e.getMessage());
e.printStackTrace();
} finally {
try {
if (os != null) {
os.close();//关闭流
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
VerifyCode :
public class VerifyCode {
public static String drawRandomText(int width, int height, BufferedImage verifyImg) {
Graphics2D graphics = (Graphics2D)verifyImg.getGraphics();
graphics.setColor(Color.WHITE);//设置画笔颜色-验证码背景色
graphics.fillRect(0, 0, width, height);//填充背景
graphics.setFont(new Font("微软雅黑", Font.BOLD, 40));
//数字和字母的组合
String baseNumLetter= "123456789abcdefghijklmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ";
StringBuffer sBuffer = new StringBuffer();
int x = 10; //旋转原点的 x 坐标
String ch = "";
Random random = new Random();
for(int i = 0;i < 4;i++){
graphics.setColor(getRandomColor());
//设置字体旋转角度
int degree = random.nextInt() % 30; //角度小于30度
int dot = random.nextInt(baseNumLetter.length());
ch = baseNumLetter.charAt(dot) + "";
sBuffer.append(ch);
//正向旋转
graphics.rotate(degree * Math.PI / 180, x, 45);
graphics.drawString(ch, x, 45);
//反向旋转
graphics.rotate(-degree * Math.PI / 180, x, 45);
x += 48;
}
//画干扰线
for (int i = 0; i <6; i++) {
// 设置随机颜色
graphics.setColor(getRandomColor());
// 随机画线
graphics.drawLine(random.nextInt(width), random.nextInt(height),
random.nextInt(width), random.nextInt(height));
}
//添加噪点
for(int i=0;i<30;i++){
int x1 = random.nextInt(width);
int y1 = random.nextInt(height);
graphics.setColor(getRandomColor());
graphics.fillRect(x1, y1, 2,2);
}
return sBuffer.toString();
}
/**
* 随机取色
*/
private static Color getRandomColor() {
Random ran = new Random();
Color color = new Color(ran.nextInt(256),
ran.nextInt(256), ran.nextInt(256));
return color;
}
}
3. 接下来正式进行整合
,用于指定验证类,加密方式等
,用于指定登录模式(FormLogin、HttpBasic)、登录页面、资源访问的权限、放行资源、给自定义过滤器指定执行顺序、退出、记住我等等@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private SysUserServiceImpl sysUserService;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
@Autowired
private MyAuthenticationFailureHandler myAuthenticationFailureHandler;
@Autowired
private MyOnecPerFilter myOnecPerFilter;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(sysUserService).passwordEncoder(passwordEncoder);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/login","/index","/getVerifyCode").permitAll()
// 注意:hasRole,hasAnyRole方法会自动加上前缀"ROLE_";而hasAnyAuthority不会
.antMatchers("/sysUser/add").hasRole("ADD")
.antMatchers("/sysUser/list").hasAnyAuthority("ROLE_LIST")
.antMatchers("/sysUser/update").hasAnyAuthority("ROLE_UPDATE")
.antMatchers("/sysUser/delete").hasAnyAuthority("ROLE_DELETE")
.antMatchers("/**").authenticated()
.and()
.formLogin().loginPage("/login")
// 认证成功后跳转的页面,与successHandler不共存
// .defaultSuccessUrl("/index")
.successHandler(myAuthenticationSuccessHandler).failureHandler(myAuthenticationFailureHandler)
.and()
.csrf().disable();
http.addFilterBefore(myOnecPerFilter,UsernamePasswordAuthenticationFilter.class);
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
《这里不知道为什么打开login页面的访问映射路径非得是login,若访问映射路径改成其他名字(例如MyLogin)并在SecurityConfiguration中对其(例如MyLogin)放行且指定登录页面是修改后的名字(例如MyLogin),那样的话,登录成功后仍然会访问登录页面而不会显示认证成功处理器返回的json,这里有哪位小伙伴知道的话可以指点一下我,谢谢!》
从代码中可以看出我们指定了加密方式BCryptPasswordEncoder,自定义认证逻辑SysUserService(该类实现UserDetailsService接口,重写loadUserByUsername方法,并在方法中定义认证逻辑),认证成功后的处理器myAuthenticationSuccessHandler,认证失败后的处理器myAuthenticationFailureHandler,以及我们将自定义的过滤器myOnecPerFilter放在了UsernamePasswordAuthenticationFilter前边执行。
认证失败后的处理器myAuthenticationFailureHandler:
@Component
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
Map<String,Object> map=new HashMap<>(2);
map.put("success",false);
if(e.getMessage()!=null){
if(e.getMessage().contains("UserDetailsService returned null")){
map.put("mes","用户名不存在");
}else if(e.getMessage().contains("Bad credentials")){
map.put("mes","账号或密码输入错误");
}else{
map.put("mes",e.getMessage());
}
}
String result = JSONObject.toJSONString(map);
httpServletResponse.setContentType("text/json;charset=UTF-8");
httpServletResponse.getWriter().write(result);
}
}
认证成功后的处理器myAuthenticationSuccessHandler:
@Component
@Slf4j
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) throws IOException, ServletException {
chain.doFilter(request,response);
}
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
Map<String,Object> map=new HashMap<>(2);
map.put("success",true);
map.put("mes","登陆成功");
String result = JSONObject.toJSONString(map);
httpServletResponse.setContentType("text/json;charset=UTF-8");
httpServletResponse.getWriter().write(result);
}
}
自定义的过滤器myOnecPerFilter:
@Component
@Slf4j
public class MyOnecPerFilter extends OncePerRequestFilter {
@Autowired
private MyAuthenticationFailureHandler authenticationFailureHandler;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
String requestURI = request.getRequestURI();
String method = request.getMethod();
log.debug("URL:" + requestURI + "----------METHOD:" + method);
if (requestURI.contains("login") && method.equalsIgnoreCase("POST")) {
try {
String imageCode = request.getParameter("imageCode");
if (StringUtils.isEmpty(imageCode)) {
throw new MyAuthenticationException("请输入验证码");
}
String verifyCode = (String) request.getSession().getAttribute("verifyCode");
if (!imageCode.equalsIgnoreCase(verifyCode)) {
throw new MyAuthenticationException("请输入正确的验证码");
}
} catch (AuthenticationException e) {
authenticationFailureHandler.onAuthenticationFailure(request, response, e);
return;
}
}
chain.doFilter(request, response);
}
}
自定义的过滤器中使用到的异常:
public class MyAuthenticationException extends AuthenticationException {
public MyAuthenticationException(String msg, Throwable t) {
super(msg, t);
}
public MyAuthenticationException(String msg) {
super(msg);
}
}
SysUserServiceImpl 中自定义认证逻辑(该类实现UserDetailsService接口,重写loadUserByUsername方法,并在方法中定义认证逻辑):
@Service("sysUserService")
public class SysUserServiceImpl implements SysUserService, UserDetailsService {
@Resource
private SysUserDao sysUserDao;
@Override
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
SysUser sysUser = sysUserDao.queryByUserName(userName);
List<GrantedAuthority> list=new ArrayList();
if(sysUser!=null){
List<SysPermission> permTags = sysUserDao.queryPermTagsByUserName(userName);
for (SysPermission permTag : permTags) {
list.add(new SimpleGrantedAuthority(permTag.getPermtag()));
}
sysUser.setAuthorities(list);
}
return sysUser;
}
}
接下来sysUserDao定义两个查询方法,并编写sql
SysUser queryByUserName(String userName);
List<SysPermission> queryPermTagsByUserName(String userName);
<!--查询单个-->
<select id="queryByUserName" resultMap="SysUserMap" parameterType="string">
select
id, username, realname, password, createDate, lastLoginTime, enabled, accountNonExpired, accountNonLocked, credentialsNonExpired
from security.sys_user
where username = #{username}
</select>
<!--根据用户名查询所有权限-->
<select id="queryPermTagsByUserName" resultType="sysPermission" parameterType="string">
SELECT sys_permission.id,sys_permission.permName,sys_permission.permTag
from (select id ,username from sys_user where username=#{userName}) user
join sys_user_role user_role on user.id=user_role.user_id
join sys_role_permission role_permission on user_role.role_id=role_permission.role_id
join sys_permission on sys_permission.id=role_permission.perm_id
</select>
最后进行访问localhost:8080/login进行登录测试即可
4. 方法及注意
方法含义
access(String) 参数为SpEL,如果返回为true则允许访问)
anonymous() 允许匿名访问
authorizerequests() 限定通过签名的请求
anyRequest() 限定任意的请求
hasAnyrole(String…) 将访问权限赋予多个角色(角色会自动加入前缀“ROLE”)
hasRole(String) 将访问权限赋予一个角色(角色会自动加入前缀“ROLE”)
permitAll() 无条件允许访问
and() 连接词,并取消之前限定前提规则
httpbasico() 启用浏览器的HTTP基础验证
formLogin() 启用 Spring Security默认的登录页面
not() 对其他方法的访问采取求反
fullyAuthenticated() 如果是完整验证(并非 Remember--me),则允许访问
dentAll() 无条件不允许任何访问
haslpAddress(String) 如果是给定的IP地址则允许访问
rememberme() 用户通过 Remember-me功能验证就允许访问
hasAuthority(String) 如果是给定的角色就允许访问(不会自动加入前缀“ROLE”)
hasAnyAuthority(String) 如果是给定的角色中的任意一个就允许访问(不会自动加入前缀“ROLE”)
http.authorizeRequests()
//允许所有人(无论是否认证)访问login、index、getVerifyCode
.antMatchers("/login", "/index", "/getVerifyCode").permitAll()
//允许角色是ROLE_ADD或者ROLE_ADMIN并且是完整登录(非记住我登录)进行访问/sysUser/add
.antMatchers("/sysUser/add").access("hasAuthority('ROLE_ADD') || hasRole('ADMIN') && isFullyAuthenticated()")
方法含义
authentication() 用户认证对象
denyAll() 拒绝任何访问
hasanyrole((String…) 当前用户是否存在参数中列明的对象属性
hasRole(String) 当前用户是否存在角色
haslpAddress(String) 是否请求来自指定的IP
isAnonymous() 是否匿名访问
isAuthenticated() 是否用户通过认证签名 isFullyAuthenticatedO是否用户是完整验证,即非“记住我”(Remember me认证)功能通过的认
isRememberMe() 是否是通过“记住我”功能通过的验证
permitAll() 无条件允许任何访问
principal() 用户的principal对象
system:
user:
password:
secret:xxxx(密钥)
在实例化加密方式采用
@Value("${system.user.password.secret}")
private String secret;
@Bean
public PasswordEncoder passwordEncoder(){
return new Pbkdf2PasswordEncoder(this.secret);
}