本教程来自:尚硅谷SpringSecurity框架教程(spring security源码剖析从入门到精通)
教程分为五大部分:框架概述、入门和基本原理、基于Web的权限方案、基于微服务的权限方案、源码剖析,详细讲解了Spring Security框架,内容由浅入深,理论实践相结合,更深入源码级学习。
前置知识:javaweb,spring,springboot.
使用springboot初始向导,在模块里选择web,security,lombok,创建好后parent改个版本,这里用2.2.1.RELEASE。
弄一个controller试试:
登录http://localhost:8080/hello,没有像预想的那样,跳hello,security,而是一个网址是http://localhost:8080/login的登录页面:
这说明spring security起效了,默认的带的用户是user,密码看后台输出。
输入用户密码后正常跳转了:
参考:https://blog.csdn.net/u012702547/article/details/89629415
本质上是个过滤器链,很明显,过滤器链就是一种责任链模式,而且在过滤器组件注入容器的过程中肯定也用了代理模式。
流程:
当什么也没有配置的时候,账号和密码是由 Spring Security 定义生成的。
而在实际项目中账号和密码都是从数据库中查询出来的。 所以我们要通过自定义逻辑控制认证逻辑,只需要实现 UserDetailsService 接口即可。
在认证环节,还要用到一个别的接口,就是PasswordEncoder接口,该接口的实现类可以对密码进行加密、匹配判断等操作。
接口中方法介绍:
实现类:
BCryptPasswordEncoder 是 Spring Security 官方推荐的密码解析器,平时多使用这个解析器。
BCryptPasswordEncoder 是对 bcrypt 强散列方法的具体实现。是基于 Hash 算法实现的单向加密。可以通过 strength 控制加密强度,默认 10。
测试该实现类:
@Test
public void test01() {
// 创建密码解析器
BCryptPasswordEncoder bCryptPasswordEncoder = new
BCryptPasswordEncoder();
// 对密码进行加密
String atguigu = bCryptPasswordEncoder.encode("atguigu");
// 打印加密之后的数据
System.out.println("加密之后数据:\t" + atguigu);
//判断原字符加密后和加密之前是否匹配
boolean result = bCryptPasswordEncoder.matches("atguigu", atguigu);
// 打印比较结果
System.out.println("比较结果:\t" + result);
}
这里给个可复制的数据,这是atguigu的密文,可以模拟从数据库里查出来的password的结果:
加密之后数据: $2a$10$JDIsewXw0fYIr/L7SKho4uWlnY8Enhk5c2zdJ2WdYz4H.V6t2b22e
比较结果: true
返回值UserDetails,也是个接口,是系统默认的用户主体:
实现这个UserDetail接口的有一个类User,用这个类就行:
User类有两个有参构造方法:
package com.atguigu.securitydemo1.service;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
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.stereotype.Service;
import java.util.List;
@Service("userDetailService")
public class MyUserDetailService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
//public User(String username, String password, Collection extends GrantedAuthority> authorities)
// username:用户名,应该是传入的s
// password:从数据库读出来的加密的密码,这里模拟从数据库取出来,是atguigu的加密结果
// authorities:权限信息
String username = s;
String password = "$2a$10$JDIsewXw0fYIr/L7SKho4uWlnY8Enhk5c2zdJ2WdYz4H.V6t2b22e";
List<GrantedAuthority> admin = AuthorityUtils.commaSeparatedStringToAuthorityList("admin");
User user = new User(s,password,admin);
return user;
}
}
package com.atguigu.securitydemo1.config;
import org.springframework.beans.factory.annotation.Autowired;
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.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Bean
//教程上这里有个@Bean,但私以为这个可不加,毕竟不是autowire注入方式使用,只在这里当个常规方法使用的话可不加,经验证不加也可以
PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
输入localhost:8080/hello在登陆窗口输atguigu,atguigu,
可以进入hello页面就算成功。
CREATE TABLE users(
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(20) UNIQUE NOT NULL,
PASSWORD VARCHAR(100)
);
INSERT INTO users VALUES(1,'张san','$2a$10$2R/M6iU3mCZt3ByG7kwYTeeW0w7/UqdeXrb27zkBIizBvAven0/na');
INSERT INTO users VALUES(2,'李si','$2a$10$2R/M6iU3mCZt3ByG7kwYTeeW0w7/UqdeXrb27zkBIizBvAven0/na');
其中密码模拟的是atguigu加密后的。
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.0.5version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druid-spring-boot-starterartifactId>
<version>1.1.17version>
dependency>
配yaml:
spring:
datasource:
url: jdbc:mysql://localhost:3306/learn_security?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&nullCatalogMeansCurrent=true
username: root
password: password
driver-class-name: com.mysql.cj.jdbc.Driver
druid:
aop-patterns: com.atguigu.securitydemo1.* #监控SpringBean
filters: stat,wall # 底层开启功能,stat(sql监控),wall(防火墙)
# 配置mybatis规则
mybatis:
#全局配置文件,autoconfig都配好了,可以不配
#config-location: classpath:mybatis/mybatis-config.xml
mapper-locations: classpath:mybatis/mapper/*.xml
configuration:
map-underscore-to-camel-case: true #开启数据库表里面的字段名xxx_yyy和实体类里面的属性名xxxYyy的自动驼峰对应。
建users表的实体类:
import lombok.Data;
@Data
public class Users {
private Integer id;
private String username;
private String password;
}
建UsersMapper接口,实现basemapper,自带基本的crud:
package com.atguigu.securitydemo1.mapper;
import com.atguigu.securitydemo1.entity.Users;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
//要么这里加@Mapper,要么启动类上加@MapperScan("com.atguigu.securitydemo1.mapper")
@Mapper
public interface UsersMapper extends BaseMapper<Users> {
}
package com.atguigu.securitydemo1.service;
import com.atguigu.securitydemo1.entity.Users;
import com.atguigu.securitydemo1.mapper.UsersMapper;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
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.stereotype.Service;
import java.util.List;
@Service("userDetailService")
public class MyUserDetailService implements UserDetailsService {
@Autowired
private UsersMapper usersMapper;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
//根据用户名查询
QueryWrapper<Users> queryWrapper = new QueryWrapper();
queryWrapper.eq("username", s);
Users users = usersMapper.selectOne(queryWrapper);
if (users == null) {
throw new UsernameNotFoundException("username is not exist");
}
String username = s;
List<GrantedAuthority> admin = AuthorityUtils.commaSeparatedStringToAuthorityList("admin");
User user = new User(s, users.getPassword(), admin);
return user;
}
}
实现WebSecurityConfigurerAdapter的SecurityConfig不用变,维持3.1.2节代码。
4. 登录测试
网址:http://localhost:8080/hello,弹出login页,输入正确的会跳到hello页就算成功,账号密码对不上会显示Bad credentials,不跳转:
上一节虽然实现了账号密码从数据库比对,但登录页是springsecurity自带的,我们要替换成自己的。
目录结构如下:
MySecurityConfig:
package com.atguigu.securityatguigu.config;
import org.springframework.beans.factory.annotation.Autowired;
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.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//自定义登陆页面
http.formLogin()
.loginPage("/login.html") //登录页
.loginProcessingUrl("/user/login") //登录数据访问的controller路径,这个路径不用配controller,是security自己在管理
.defaultSuccessUrl("/test/index").permitAll() //登陆成功后跳转的路径
.and().authorizeRequests()
.antMatchers("/","/test/hello","/user/login").permitAll() //设置不需要认证即可访问的路径
.anyRequest().authenticated()
.and().csrf().disable(); //关闭csrf防护
}
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
TestController:
package com.atguigu.securityatguigu.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/test")
public class TestController {
@GetMapping("/hello")
public String hello() {
return "hello,security";
}
@GetMapping("/index")
public String index() {
return "hello,index";
}
}
login.html:
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Titletitle>
head>
<body>
<h2>mylogin-pageh2>
<form action="/user/login" method="post">
username:<input type="text" name="username"><br>
password:<input type="password" name="password"><br>
<input type="submit" value="登录">
form>
body>
html>
其他的参考上节案例。
每个用户的访问权限我们是通过给不同的用户赋予不同角色,角色不同,权限不同。
这就是为什么我们要给每个user一个角色,而不是单独规定user的访问权限。
一般我们会设计5个表:
在MySecurityConfig中添加:
数据库users表多加一列role,实体类也跟着改,MyUserDetailService也改:
测试使用不同role的账号登陆,当使用李si账号访问index页会报403错误,就是权限不足:
hasAuthority(String authority)只能控制一种角色,当一个url多种角色都可以访问时,可以用hasAnyAuthority(String… authorities):
角色和权限控制的注解形式:
角色:
@Secured({“ROLE_admin”,“ROLE_normal”})
@PreAuthorize(“hasAnyRole(“ROLE_admin”,“ROLE_normal”)”)
权限:
@PreAuthorize(“hasAnyAuthority(“admin”,“normal”)”)
@PostAuthorize(“hasAnyAuthority(“admin”,“normal”)”) :使用并不多,在方法执行后再进行权限验证,适合验证带有返回值
的权限。
使用基于权限认证进行测试时,不要忘了改MyUserDetailService,去掉ROLE_前缀:
过滤:
建个index.html做测试:
修改MySecurityConfig:
登陆跳转设为index.html(不知道为什么这个在登陆后不起效。。。),
不需要认证的路径去掉“/”,
增加注解@EnableGlobalMethodSecurity(securedEnabled = true)
package com.atguigu.securityatguigu.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true)
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//自定义登陆页面
http.formLogin()
.loginPage("/login.html") //登录页
.loginProcessingUrl("/user/login") //登录数据访问的controller路径,这个路径不用配controller,是security自己在管理
.defaultSuccessUrl("/index.html").permitAll() //登陆成功后跳转的路径
.and().authorizeRequests()
.antMatchers( "/test/hello", "/user/login").permitAll() //设置不需要认证即可访问的路径
.anyRequest().authenticated()
.and().csrf().disable(); //关闭csrf防护
http.exceptionHandling().accessDeniedPage("/unauth.html");
}
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
TestController在原来基础上加两个路径:
@GetMapping("/admin")
@Secured({"ROLE_admin"})
public String admin(){
return "admin-page";
}
@GetMapping("/normal")
@Secured({"ROLE_admin","ROLE_normal"})
public String normal(){
return "normal-page";
}
测试登录role是normal的李si用户,发现点击admin的链接是自定义的403,normal可以正常进入。
之前在web我们见过一种自动登录的实现形式—基于cookie,这里介绍通过另一种方式(JWT(json web token))实现自动登录。
参考:
Token登录认证详解
JWT的原理和使用
JWT(JSON Web Token)简介
什么是CSRF?可能多数人都不清楚,没事,一起来了解!!!
认证里面很重要的一点就是单点登录,就是说一个用户在一个微服务模块登录后访问其他模块不需要登陆。
认证授权过程:
权限管理模型(RBAC方式):
教学代码下载:https://download.csdn.net/download/anotherQu/49600330
sql代码可以取自:一个atguigu学生的gitee-springsecurity代码笔记
主要实现三点:
使用技术:
教学代码下载:https://download.csdn.net/download/anotherQu/49600330
sql代码可以取自:一个atguigu学生的gitee-springsecurity代码笔记
redis和nacos的安装和启动请参考尚硅谷相关的视频教程。
其他模块不做说明,这里只展示security模块的相关代码
TokenWebSecurityConfig:
package com.atguigu.security.config;
import com.atguigu.security.filter.TokenAuthFilter;
import com.atguigu.security.filter.TokenLoginFilter;
import com.atguigu.security.security.DefaultPasswordEncoder;
import com.atguigu.security.security.TokenLogoutHandler;
import com.atguigu.security.security.TokenManager;
import com.atguigu.security.security.UnauthEntryPoint;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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.core.userdetails.UserDetailsService;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
//security总配置类
public class TokenWebSecurityConfig extends WebSecurityConfigurerAdapter {
private UserDetailsService userDetailsService;
private DefaultPasswordEncoder defaultPasswordEncoder;
private TokenManager tokenManager;
private RedisTemplate redisTemplate;
@Autowired
public TokenWebSecurityConfig(UserDetailsService userDetailsService,
DefaultPasswordEncoder defaultPasswordEncoder,
TokenManager tokenManager,
RedisTemplate redisTemplate) {
this.userDetailsService = userDetailsService;
this.defaultPasswordEncoder = defaultPasswordEncoder;
this.tokenManager = tokenManager;
this.redisTemplate = redisTemplate;
}
//设置退出的地址和token,redis操作地址
@Override
protected void configure(HttpSecurity http) throws Exception {
http.exceptionHandling()
//没有权限访问时调用自定义的处理类
.authenticationEntryPoint(new UnauthEntryPoint())
.and().csrf().disable()
.authorizeRequests()
.anyRequest().authenticated()
//退出路径
.and()
.logout().logoutUrl("/admin/acl/index/logout")
// 调用退出时的处理器
.addLogoutHandler(new TokenLogoutHandler(tokenManager, redisTemplate))
.and()
// 认证过滤器
.addFilter(new TokenLoginFilter(authenticationManager(), tokenManager, redisTemplate))
// 授权过滤器
.addFilter(new TokenAuthFilter(authenticationManager(), tokenManager, redisTemplate))
.httpBasic();
}
@Override
//调用userDetailsService和密码处理
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(defaultPasswordEncoder);
}
@Override
//不进行认证的路径,可以直接访问
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/api/**");
}
}
CurrentUserInfo:
package com.atguigu.security.entity;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
@Data
@ApiModel(description = "用户实体类")
public class CurrentUserInfo implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "微信openid")
private String username;
@ApiModelProperty(value = "密码")
private String password;
@ApiModelProperty(value = "昵称")
private String nickName;
@ApiModelProperty(value = "用户头像")
private String salt;
@ApiModelProperty(value = "用户签名")
private String token;
}
SecurityUser:
package com.atguigu.security.entity;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.util.StringUtils;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@Data
public class SecurityUser implements UserDetails {
//当前登录用户
private transient CurrentUserInfo currentUserInfo;
//当前权限
private List<String> permissionValueList;
public SecurityUser() {
}
public SecurityUser(CurrentUserInfo user) {
if (user != null) {
this.currentUserInfo = user;
}
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authorities = new ArrayList<>();
for (String permissionValue : permissionValueList) {
if (StringUtils.isEmpty(permissionValue)) {
continue;
}
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permissionValue);
authorities.add(authority);
}
return authorities;
}
@Override
public String getPassword() {
return currentUserInfo.getPassword();
}
@Override
public String getUsername() {
return currentUserInfo.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
TokenAuthFilter:
package com.atguigu.security.filter;
import com.atguigu.security.security.TokenManager;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
//授权过滤器
@SuppressWarnings("unchecked")
public class TokenAuthFilter extends BasicAuthenticationFilter {
private TokenManager tokenManager;
private RedisTemplate redisTemplate;
public TokenAuthFilter(AuthenticationManager authenticationManager,
TokenManager tokenManager,
RedisTemplate redisTemplate) {
super(authenticationManager);
this.tokenManager = tokenManager;
this.redisTemplate = redisTemplate;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain chain) throws IOException, ServletException {
// 获取当前认证成功的用户权限信息
UsernamePasswordAuthenticationToken authRequest = getAuthentication(request);
// 有权限信息 放入权限上下文中
if (authRequest != null) {
SecurityContextHolder.getContext().setAuthentication(authRequest);
}
chain.doFilter(request, response);
}
//从token获取用户名,从redis获取对应权限列表
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
//从header获取token
String token = request.getHeader("token");
if (token == null) {
return null;
}
//从token获取用户名
String username = tokenManager.getUserInfoFromToken(token);
//从redis获取对应权限列表
List<String> list = (List<String>) redisTemplate.opsForValue().get(username);
if (null == list) {
return null;
}
Collection<GrantedAuthority> authorities = new ArrayList<>();
list.forEach(item -> {
authorities.add(new SimpleGrantedAuthority(item));
});
return new UsernamePasswordAuthenticationToken(username, token, authorities);
}
}
TokenLoginFilter
package com.atguigu.security.filter;
import com.atguigu.security.entity.CurrentUserInfo;
import com.atguigu.security.entity.SecurityUser;
import com.atguigu.security.security.TokenManager;
import com.atguigu.utils.utils.R;
import com.atguigu.utils.utils.ResponseUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
//认证过滤器
public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {
/**
* 权限
*/
private AuthenticationManager authenticationManager;
private TokenManager tokenManager;
private RedisTemplate redisTemplate;
public TokenLoginFilter(AuthenticationManager authenticationManager,
TokenManager tokenManager,
RedisTemplate redisTemplate) {
this.authenticationManager = authenticationManager;
this.tokenManager = tokenManager;
this.redisTemplate = redisTemplate;
this.setPostOnly(false);
// 设置登陆路径,并且post请求
this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/admin/acl/login", "POST"));
}
@Override
//获取表单提交的相关信息
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
// 获取表单提交数据
try {
CurrentUserInfo user = new ObjectMapper().readValue(request.getInputStream(), CurrentUserInfo.class);
return authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
user.getUsername(), user.getPassword(), new ArrayList<>()));
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException();
}
}
@Override
//认证成功调用的方法 生成token 存入到redis
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain,
Authentication authResult) throws IOException, ServletException {
// 认证成功之后,得到认证成功后的用户信息
SecurityUser user = (SecurityUser) authResult.getPrincipal();
// 根据用户名生成token
String token = tokenManager.createToken(user.getUsername());
// 把用户名和用户权限放入redis中
redisTemplate.opsForValue().set(user.getCurrentUserInfo().getUsername(), user.getPermissionValueList());
ResponseUtil.out(response, R.ok().data("token", token));
}
@Override
//认证失败调用的方法
protected void unsuccessfulAuthentication(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException failed) throws IOException, ServletException {
ResponseUtil.out(response, R.error());
}
}
DefaultPasswordEncoder:
package com.atguigu.security.security;
import com.atguigu.utils.utils.MD5;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
@Component
//密码处理工具类
public class DefaultPasswordEncoder implements PasswordEncoder {
public DefaultPasswordEncoder() {
this(-1);
}
public DefaultPasswordEncoder(int strength) {
}
//MD5加密
@Override
public String encode(CharSequence charSequence) {
return MD5.encrypt(charSequence.toString());
}
//密码比对
@Override
public boolean matches(CharSequence charSequence, String encodedPassword) {
return encodedPassword.equals(MD5.encrypt(charSequence.toString()));
}
@Override
public boolean upgradeEncoding(String encodedPassword) {
return false;
}
}
TokenLogoutHandler:
package com.atguigu.security.security;
import com.atguigu.utils.utils.R;
import com.atguigu.utils.utils.ResponseUtil;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
//退出处理器 退出时移除token 并删除redis中的token信息
public class TokenLogoutHandler implements LogoutHandler {
private TokenManager tokenManager;
private RedisTemplate redisTemplate;
public TokenLogoutHandler(TokenManager tokenManager, RedisTemplate redisTemplate) {
this.tokenManager = tokenManager;
this.redisTemplate = redisTemplate;
}
@Override
public void logout(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) {
//1 从header里面获取token
//2 token不为空,移除token,从redis删除token
String token = httpServletRequest.getHeader("token");
if (token != null) {
//移除
tokenManager.removeToken(token);
//从token获取用户名
String username = tokenManager.getUserInfoFromToken(token);
redisTemplate.delete(username);
}
ResponseUtil.out(httpServletResponse, R.ok());
}
}
TokenManager:
package com.atguigu.security.security;
import io.jsonwebtoken.CompressionCodecs;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.stereotype.Component;
import java.util.Date;
@Component
public class TokenManager {
//token有效时常 毫秒单位
private long tokenEcpiration = 24 * 60 * 60 * 1000;
//编码密钥,应该是自动生成,这里为了简单写死
private String tokenSignKey = "123456";
//根据用户名生成token
public String createToken(String username) {
String token = Jwts.builder()
.setSubject(username)
.setExpiration(new Date(System.currentTimeMillis() + tokenEcpiration))
.signWith(SignatureAlgorithm.HS512, tokenSignKey)
.compressWith(CompressionCodecs.GZIP)
.compact();
return token;
}
//根据token字符串得到用户信息
public String getUserInfoFromToken(String token) {
String userinfo = Jwts.parser()
.setSigningKey(tokenSignKey)
.parseClaimsJws(token)
.getBody()
.getSubject();
return userinfo;
}
//删除token
public void removeToken(String token) {
}
}
UnauthEntryPoint:
package com.atguigu.security.security;
import com.atguigu.utils.utils.R;
import com.atguigu.utils.utils.ResponseUtil;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class UnauthEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
ResponseUtil.out(httpServletResponse, R.error());
}
}
根路径下:
npm install
npm run dev
前端启动:npm run dev
后端启动:nacos,redis,gateway模块,service_acl模块
访问:
http://localhost:9528