项目介绍
编程区新手,未来继续创造新项目
github仓库:github.com/neutron123a…
github主页:github.com/neutron123a…
某天突发奇想,想做一个稍微完善点的登录体系,所以就开始编写这个项目,但现在这个项目还比较粗糙,后续我会不断完善,以后写项目就能够直接拿过来用了。
思路
首要介绍一下这个登录体系的大致思路:
前端先进行注册操作,为了数据传输安全,这儿会先对前端输入的暗码进行RSA加密,所用的公钥从后端接口中获取。后端接受到数据后运用密钥进行RSA解密,得到了暗码明文。然后再对暗码进行BCrypt
加密,和用户信息一起存入数据库中。有人或许会问为什么这儿不直接将前端加密后的暗码存入数据库中,我的主意是:假如直接存储前端密文,别人不就有时机能直接获取数据库里面的数据了吗,所以仍是把前端密文当做是一个前后端数据传输的临时产物吧。
这儿再附上我的RSA东西类:
@Getter
public class RSAUtils {
private final String publicKey;
private final String privateKey;
private static RSAUtils rsaUtils;
private static RSA rsa;
/**
* 生成密钥对
*/
private RSAUtils(){
rsa = new RSA();
publicKey = rsa.getPublicKeyBase64();
privateKey = rsa.getPrivateKeyBase64();
}
/**
* 单例形式取得东西类实例,避免频繁生成密钥对
* @return 实例
*/
public static RSAUtils getRsaUtils(){
if(rsaUtils == null){
rsaUtils = new RSAUtils();
}
return rsaUtils;
}
/**
* 解密
* @param password 密文
* @return 明文
*/
public String decodePassword(String password){
String publicKey = rsaUtils.getPublicKey();
String privateKey = rsaUtils.getPrivateKey();
RSA rsa = new RSA(privateKey, publicKey);
byte[] decrypt = rsa.decrypt(password, KeyType.PrivateKey);
return new String(decrypt);
}
}
然后便是登录功用了。我的完结计划是:用户输入完用户名、暗码后,后端进行校验,校验通往后Spring Security会生成一个认证信息。然后生成一个UUID字符串,将其作为key,前面的认证信息作为value存入redis中,同时为其设置一个过期时刻,这个时刻便是用户登录凭据的过期时刻。之后再将前面的UUID作为payload,生成一个JWT字符串,然后运用jdk自带的keytool东西生成的证书文件对其进行签名,这儿我运用的是nimbus-jose-jwt
,它很适合进行RSA的签名和验签操作。
登录成功后,会将这个JWT字符串回来给前端,前端之后每次恳求都会携带着这个JWT字符串,后端对其进行验签,若验签经过,则解析JWT得到payload字段中的内容,也便是上面生成的UUID,在依据这个UUID去redis中查找用户信息,若能找到,则会进行一次Spring Security的验证操作,使其能够拜访接口(后边还有授权操作);若查找失败,则阐明用户凭据过期,需求从头登录。
具体完结
跨域问题
因为项目时前后端别离,所以必然存在着跨域问题,现在首要的解决计划是:
- 在办法上增加
@CrossOrigin
注解 - 装备过滤器
但是,在引入SpringSecurity后,上面两种办法都会失效,因为SpringSecurity拦截器的优先级更高,上面两种办法都会被拦截,解决办法是提高跨域过滤器的优先级,要勇于SpringSecrity拦截器的优先级。
但Spring Security有更加专业的跨域解决办法:
//跨域
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedMethod("*");
corsConfiguration.addAllowedOrigin("*");
corsConfiguration.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", corsConfiguration);
return source;
}
然后将该过滤器注册到spring security的过滤器链中即可
数据库表规划
因为后边还要运用RBAC0
模型,所以我一个规划了七张表,别离是:
- 用户表
- 人物表
- 权限表
- 资源表
- 用户人物相关表
- 人物权限相关表
- 权限资源相关表
具体SQL如下:
# 用户表
CREATE TABLE IF NOT EXISTS `user`
(
`id` INT(11) NOT NULL AUTO_INCREMENT COMMENT '用户id',
`username` VARCHAR(32) DEFAULT NULL COMMENT '用户名',
`password` VARCHAR(255) DEFAULT NULL COMMENT '加密后的暗码',
`enabled` TINYINT(1) DEFAULT NULL COMMENT '账户是否可用',
`accountNonExpired` TINYINT(1) DEFAULT NULL COMMENT '账户是否没有过期',
`accountNonLocked` TINYINT(1) DEFAULT NULL COMMENT '账户是否没有锁定',
`credentialNonExpired` TINYINT(1) DEFAULT NULL COMMENT '凭据是否没有过期',
PRIMARY KEY (`id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
# 人物表
CREATE TABLE IF NOT EXISTS `role`
(
`id` INT(11) NOT NULL AUTO_INCREMENT COMMENT '人物id',
`name` VARCHAR(32) DEFAULT NULL COMMENT '人物英文名',
`nameZh` VARCHAR(32) DEFAULT NULL COMMENT '人物中文名',
PRIMARY KEY (`id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
# 权限表
CREATE TABLE IF NOT EXISTS `permission`
(
`id` INT(11) NOT NULL AUTO_INCREMENT COMMENT '权限id',
`name` VARCHAR(32) DEFAULT NULL COMMENT '权限英文名',
`nameZh` VARCHAR(32) DEFAULT NULL COMMENT '权限中文名',
PRIMARY KEY (`id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
# 资源表
CREATE TABLE IF NOT EXISTS `resources`
(
`id` INT(11) NOT NULL AUTO_INCREMENT COMMENT '权限id',
`name` VARCHAR(32) DEFAULT NULL COMMENT '权限中文名',
`url` varchar(32) DEFAULT NULL COMMENT '接口地址',
PRIMARY KEY (`id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
# 用户-人物相关表
CREATE TABLE IF NOT EXISTS `user_role`
(
`id` INT(11) NOT NULL AUTO_INCREMENT COMMENT '表id',
`user_id` INT(11) DEFAULT NULL COMMENT '用户id',
`role_id` INT(11) DEFAULT NULL COMMENT '人物id',
PRIMARY KEY (`id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
# 人物-权限相关表
CREATE TABLE IF NOT EXISTS `role_permission`
(
`id` INT(11) NOT NULL AUTO_INCREMENT COMMENT '表id',
`role_id` INT(11) DEFAULT NULL COMMENT '人物id',
`permission_id` INT(11) DEFAULT NULL COMMENT '权限id',
PRIMARY KEY (`id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
# 权限-资源相关表
CREATE TABLE IF NOT EXISTS `permission-resources`
(
`id` INT(11) NOT NULL AUTO_INCREMENT COMMENT '表id',
`permission_id` INT(11) DEFAULT NULL COMMENT '权限id',
`resources_id` INT(11) DEFAULT NULL COMMENT '资源id',
PRIMARY KEY (`id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
对应的实体类如下:
用户实体类
这儿首要留意一下用户实体类中获取权限的办法,因为选用的是RBAC0模型,所以权限信息都与人物相相关,咱们要先遍历用户具有的一切人物,然后将该人物具有的一切权限取出,寄存到SimpleGrantedAuthority调集中,最后再将该调集增加到GrantedAuthority调集中,即可完结用户与权限的绑定
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements UserDetails {
private Integer id;
private String username;
private String password;
private boolean enabled;
private boolean accountNonExpired;
private boolean accountNonLocked;
private boolean credentialNonExpired;
private List<Role> roles; //用户所具有的人物
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
for (Role role : roles) {
List<SimpleGrantedAuthority> roleAuthorities = new ArrayList<>();
for (Permission permission : role.getPermissions()) {
//保存权限信息
roleAuthorities.add(new SimpleGrantedAuthority(permission.getName()));
}
authorities.addAll(roleAuthorities);
}
return authorities;
}
@Override
public boolean isAccountNonExpired() {
return accountNonExpired;
}
@Override
public boolean isAccountNonLocked() {
return accountNonLocked;
}
@Override
public boolean isCredentialsNonExpired() {
return credentialNonExpired;
}
@Override
public boolean isEnabled() {
return enabled;
}
}
人物实体类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Role implements Serializable {
private Integer id;
private String name; //人物名
private String nameZh; //人物中文名
private List<Permission> permissions; //人物所具有的权限
}
权限实体类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Permission implements Serializable {
private Integer id;
private String name; //权限名
private String nameZh; //权限中文名
}
资源实体类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Resources implements Serializable {
private Integer id;
private String name;//权限名
private String url; //接口地址
private List<Permission> permissions; //拜访受维护目标所需求的权限
}
或许会有人对资源实体类存在疑问:为什么人物相关权限是在人物实体类中运用List<Permission>
,而权限相关资源却是在资源实体类中运用List<Permission>
,变成了“资源相关权限”。其实,这并不是任意而为的,在后边的授权操作中,咱们需求为一切的受维护资源别离相关上一切能够拜访它们的权限,所以,运用“资源相关权限”这种办法能够便利后边的操作,在给受维护资源增加拜访权限时,咱们只需求运用resources.getPermissions()
就能获取到该资源具有的一切权限了。
MyBatis运用
为了强化自己动手写SQL的能力,我挑选了MyBatis作为ORM框架,但登录功用的sql仍是比较简单的。
mapper接口
@Mapper
public interface UserMapper {
//注册,增加用户
Integer addUser(@Param("username") String username, @Param("password") String password);
//依据用户名查询是否有该用户
User queryUserByUsername(String username);
//给用户绑定人物
List<Role> getRolesByUserId(@Param("user_id") Integer user_id);
//获取资源
List<Resources> getAllResources();
}
mapper.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.neutron.login_backend.mapper.UserMapper">
<!--Integer addUser(@Param("username") String username, @Param("password") String password);-->
<insert id="addUser">
INSERT INTO security.user(username, password, enabled, accountNonExpired, accountNonLocked, credentialNonExpired) VALUES(#{username}, #{password}, 1, 1, 1, 1);
</insert>
<!--User queryUserByUsername(String username);-->
<select id="queryUserByUsername" resultType="com.neutron.login_backend.entity.User">
SELECT *
FROM security.user
WHERE username = #{username}
</select>
<!--List<Role> getRolesByUserId(@Param("user_id") Integer user_id);-->
<select id="getRolesByUserId" resultMap="RoleResultMap">
SELECT role.*, permission.id as pid, permission.name as pname, permission.nameZh as pnameZh
FROM security.role role
LEFT JOIN security.user_role ur
ON role.id = ur.role_id AND ur.user_id = #{user_id}
LEFT JOIN security.role_permission rp
ON role.id = rp.role_id
LEFT JOIN security.permission permission
ON permission.id = rp.permission_id;
</select>
<resultMap id="RoleResultMap" type="com.neutron.login_backend.entity.Role">
<id property="id" column="id"/>
<result property="name" column="name"/>
<result property="nameZh" column="nameZh"/>
<collection property="permissions" ofType="com.neutron.login_backend.entity.Permission">
<id property="id" column="pid"/>
<result property="name" column="pname"/>
<result property="nameZh" column="pnameZh"/>
</collection>
</resultMap>
<!--List<Permission> getAllPermissions();-->
<select id="getAllResources" resultMap="ResourcesResultMap">
SELECT resources.*, p.id as pid, p.name as pname, p.nameZh as pnameZh
FROM security.resources resources
LEFT JOIN security.`permission-resources` pr
ON resources.id = pr.resources_id
LEFT JOIN security.permission p
ON pr.permission_id = p.id
</select>
<resultMap id="ResourcesResultMap" type="com.neutron.login_backend.entity.Resources">
<id property="id" column="id"/>
<result property="name" column="name"/>
<result property="url" column="url"/>
<collection property="permissions" ofType="com.neutron.login_backend.entity.Permission">
<id property="id" column="pid"/>
<result property="name" column="pname"/>
<result property="nameZh" column="pnameZh"/>
</collection>
</resultMap>
</mapper>
注册
前端代码,加密
这儿要导入jsencrypt包
function onClickSignUp(){
if(show.value === true){
//获取rsa公钥
let publicKey;
axios({
method: 'get',
url: 'http://localhost:8081/getPublicKey',
}).then(function (resp){
publicKey = resp.data.data
//rsa加密
let encrypt = new JSEncrypt();
encrypt.setPublicKey(publicKey);
let encodePassword = encrypt.encrypt(formLabelAlign.password);
axios({
method: 'post',
url: 'http://localhost:8081/login/signUp',
data: {
username: formLabelAlign.username,
password: encodePassword
}
}).then(function (resp){
console.log(resp.data.data)
})
})
show.value = false;
} else {
show.value = true;
}
}
后端代码,解密,供给加密公钥
/**
* 注册账号
* @param user 恳求体(用户名,暗码)
* @return 状况码
*/
@PostMapping("/signUp")
public Result<String> signUp(@RequestBody User user){
//rsa解密
String rawPassword = loginService.decodePassword(user.getPassword());
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
//bcrypt加密
String password = encoder.encode(rawPassword);
if(userMapper.addUser(user.getUsername(), password) > 0){
return Result.success("200");
}
return Result.error("400");
}
/**
* 获取公钥
* @return 公钥
*/
@GetMapping("/getPublicKey")
public Result<String> getPublicKey(){
RSAUtils rsaUtils = RSAUtils.getRsaUtils();
return Result.success(rsaUtils.getPublicKey());
}
登录
登录部分的难点首要是在前后端别离的情况下,怎么让前端的每次恳求都能被Spring Security认证,以及JWT的签名和验签操作,下面我会具体分析。
自定义暗码加密计划
在暗码加密计划这块我挑选了好久,现在干流的加密计划有:MD5,BCrypt,RSA,SCrypt等。
MD5尽管是不可逆的加密计划,但现在它很容易被彩虹表破解。而数据库中寄存的暗码最好是不可逆的,即无法或很难被解密,所以像RSA这类能够经过密钥解密的算法也被我放弃了。终究我运用了安全性较好的BCrypt算法,该算法尽管也存在被彩虹表解密的风险,但需求黑客付出极大的时刻本钱,从性价比来看,它是能够接受的。
在最新的SpringSecurity中,因为WebSecurityConfigurerAdapter
被废弃,所以咱们需求在AuthenticationManager中增加自定义的暗码加密计划PasswordEncoder
,示例如下:
@Bean
public PasswordEncoder passwordEncoder() {
//将SpringSecurity的加密计划改为BCrypt
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager() {
DaoAuthenticationProvider dao = new DaoAuthenticationProvider();
dao.setUserDetailsService(userService);
//将加密计划加入到AuthenticationManager中
dao.setPasswordEncoder(passwordEncoder());
return new ProviderManager(dao);
}
public interface UserService extends UserDetailsService {
@Override
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
登录验证
首要创立一个UsernamePasswordAuthenticationToken
目标,并在参数中带上前端传过来的用户名和暗码,之后将该目标交给AuthenticationManager
进行认证操作
UsernamePasswordAuthenticationToken token =
new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword());
Authentication authenticate = authenticationManager.authenticate(token);
if(Objects.isNull(authenticate)){
throw new RuntimeException("用户名或暗码过错");
}
SpringSecurity的具体认证流程是:
- 执行authenticate(token)办法
- 因为是运用用户名/暗码的办法登录,
AuthenticationProvider
的完结类是DaoAuthenticationProvider
- 执行
DaoAuthenticationProvider
中的retrieveUser
办法,在该办法中,会执行咱们前面承继UserDetailsService
接口时重写的loadUserByUsername
办法,去查找是否有该用户,假如没有,则抛出反常,否则就将用户信息回来 - 因为
DaoAuthenticationProvider
是承继自AbstractUserDetailsAuthenticationProvider
并且没有重写authenticate
办法,所以具体的认证逻辑位于AbstractUserDetailsAuthenticationProvider
中 - 在
AbstractUserDetailsAuthenticationProvider
的retrieve
办法中,会先去用户缓存中查找用户目标,假如查不到,就依据用户名调用retrieveUser
办法,从数据库中加载用户,假如没有加载出用户,则会抛出反常 - 拿到用户目标后,先调用
preAuthenticationChecks.check(user)
办法检查用户状况,这个状况便是咱们在User实体类中定义的accountNonExpired
这些 - 用户状况检查通往后,又调用了
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication)
办法进行暗码的校验,此处正好解答了为什么在调用loadUserByUsername时也会完结对暗码的校验的疑问。 - 最后调用
postAuthenticationChecks.check
办法检查暗码是否过期 - 当一切过程完结后,调用
createSuccessAuthentication
办法创立一个认证后的UsernamePasswordAuthenticationToken
目标,该目标中包含了认证主体、凭据以及人物信息。
生成JWT
当时面的登录成功后,咱们就要生成一个JWT字符串作为前端拜访凭据。
首要,咱们要取得一个jks证书文件,它能够帮咱们办理公钥和私钥,这儿运用的是jdk自带的keytool东西,在jdk的bin目录下面就能找到,能够运用如下指令生成证书:
keytool -genkey -alias jwt -keyalg RSA -keystore jwt.jks
有人或许会问,这儿的RSA密钥对为什么不运用前面前端暗码加密时用的密钥对。这儿我首要是想将二者分开,且前端登录时用的密钥对是不固定的,每次登录时都会从头生成密钥对,假如在这儿也运用动态的密钥对,或许会带来功能问题。
这儿附上我的JWT生成、前面和验签办法:
@Service
public class JwtTokenServiceImpl implements JwtTokenService {
/**
* 获取密钥对
* @return RSAKey
*/
@Override
public RSAKey generateRsaKey() {
KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("jwt.jks"), "123456".toCharArray());
KeyPair keyPair = keyStoreKeyFactory.getKeyPair("jwt", "123456".toCharArray());
//RSA公钥
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
//RSA私钥
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
return new RSAKey.Builder(publicKey).privateKey(privateKey).build();
}
/**
* 生成JWT字符串
* @param payloadStr 作为payload的信息
* @param rsaKey 密钥对
* @return jwt字符串
*/
@Override
public String generateTokenByRSA(String payloadStr, RSAKey rsaKey) throws JOSEException {
//JWS头
JWSHeader jwsHeader = new JWSHeader.Builder(JWSAlgorithm.RS256)
.type(JOSEObjectType.JWT)
.build();
//荷载
Payload payload = new Payload(payloadStr);
//签名
JWSObject jwsObject = new JWSObject(jwsHeader, payload);
//生成签名器
RSASSASigner rsassaSigner = new RSASSASigner(rsaKey);
jwsObject.sign(rsassaSigner);
return jwsObject.serialize();
}
/**
* 验签
* @param token jwt字符串
* @param rsaKey rsaKey
* @return 荷载信息
* @throws ParseException
* @throws JOSEException
*/
@Override
public String verifyToken(String token, RSAKey rsaKey) throws ParseException, JOSEException {
//由jwt字符串生成jwsObject目标
JWSObject jwsObject = JWSObject.parse(token);
RSAKey publicKey = rsaKey.toPublicJWK();
RSASSAVerifier verifier = new RSASSAVerifier(publicKey);
if(!jwsObject.verify(verifier)){
return null; //验证失败则回来空
}
return jwsObject.getPayload().toString();
}
}
然后,咱们取得UUID
和认证之后的用户信息,并将其存入Redis
中,同时设置过期时刻:
@PostMapping
public Result<String> login(@RequestBody User user) throws JOSEException {
UsernamePasswordAuthenticationToken token =
new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword());
Authentication authenticate = authenticationManager.authenticate(token);
if(Objects.isNull(authenticate)){
throw new RuntimeException("用户名或暗码过错");
}
//登录成功,回来JWT字符串
RSAKey rsaKey = jwtTokenService.generateRsaKey();
String key = UUID.randomUUID().toString();
//将用户信息存入redis中
User userInfo = (User) authenticate.getPrincipal();
redisTemplate.opsForValue().set(key, userInfo, 3, TimeUnit.HOURS);
return Result.success(jwtTokenService.generateTokenByRSA(key, rsaKey));
}
之后前端获取到jwt,为了便利办理,我将JWT寄存在了vuex的state中:
function onClick(){
axios({
method: 'post',
url: "http://localhost:8081/login",
data: {
...formLabelAlign
},
headers: {
'Access-Control-Allow-Origin': '*',
}
}).then(function (resp){
console.log(resp.data)
if(resp.data !== null){
store.commit('setAuthorization', resp.data.data) //vuex状况办理
router.push('/index')
}
})
}
vuex:
import {createStore} from "vuex";
const store = createStore({
state: {
"Authorization": ''
},
mutations: {
setAuthorization(state, newVal){
state.Authorization = newVal
}
}
})
export default store
前端获取到JWT之后需求在一切恳求的恳求头中都携带上Authorization
字段,这儿我运用了Axios
的拦截器:
import axios from "axios";
import store from "../store";
axios.interceptors.request.use(config => {
config.headers['Authorization'] = store.state.Authorization
return config
})
这样,一切的前端工作就现已完结了,接下来需求在后端定义一个过滤器,让除了登录接口的一切接口都需求被验证JWT合法性,具体流程是:过滤器解析JWT、取出payload字段(前面存的UUID)、从redis中取出用户认证信息、将该认证信息作为参数创立一个UsernamePasswordAuthenticationToken
目标、在SecurityContextHolder
中增加该目标的认证、认证成功、过滤器放行、后端接口正常拜访
过滤器代码
@Component
public class LoginFilter extends OncePerRequestFilter {
@Autowired
private JwtTokenService jwtTokenService;
@Resource
private RedisTemplate<String, User> redisTemplate;
@Autowired
private CustomSecurityMetadataSource customSecurityMetadataSource;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String authorization = request.getHeader("Authorization");
if(request.getRequestURI().equals("/login") || request.getRequestURI().equals("/getPublicKey")){
filterChain.doFilter(request, response);
} else if(authorization == null){
throw new RuntimeException("用户未登录");
} else{
RSAKey rsaKey = jwtTokenService.generateRsaKey();
//验签失败回来false
//成功回来true
String payload = "";
try {
payload = jwtTokenService.verifyToken(authorization, rsaKey);
} catch (ParseException | JOSEException e) {
e.printStackTrace();
}
if(payload.equals("")){
throw new RuntimeException("用户未登录");
} else {
//已登录,获取用户信息,进行授权
User userInfo = redisTemplate.opsForValue().get(payload); //取缓存
if(userInfo == null){
//用户凭据过期
redisTemplate.delete(payload);//删去用户登录信息
SecurityContextHolder.clearContext(); //将用户认证信息删去
} else {
UsernamePasswordAuthenticationToken token =
new UsernamePasswordAuthenticationToken(userInfo, null, userInfo.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(token);
System.out.println("SecurityContextHolder信息:" + SecurityContextHolder.getContext());
}
System.out.println("attributes: "+customSecurityMetadataSource.getAllConfigAttributes());
filterChain.doFilter(request, response);
}
}
}
}
至此,前端现已能经过一个JWT字符串拜访后端接口了
RBAC权限模型
接下来便是对接口拜访的授权操作了,这儿我运用的是RBAC0
权限模型,即用户相关人物,人物相关权限,权限相关资源,一切的用户、人物、权限和资源都寄存在数据库中。在这儿我想说的是,有很多人都运用了根据办法的权限办理,即在办法上经过注解的办法增加权限,我以为这种办法并不灵敏,假如想要修正拜访某个受维护资源所需求的权限时,就必须要去修正源代码,而运用根据Url的权限办理后,咱们能经过直接修正数据库完结权限的修正。
用户的授权都现已在前面的User实体类中给出,下面首要看看怎么对受维护资源增加拜访权限,这儿咱们首要是做两件事:
- 自定义权限元数据
- 增加决策器
自定义权限元数据 承继FilterInvocationSecurityMetadataSource接口
权限元数据中寄存的便是受维护资源所需求的权限,这儿咱们先取得当时恳求的uri地址,然后与数据库中的受维护资源的地址进行比较,若相同,则将数据库中与之对应的权限信息增加进去
@Component
public class CustomSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
@Autowired
private UserMapper userMapper;
AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
//获取URI
String requestURI = ((FilterInvocation) object).getRequest().getRequestURI();
List<Resources> allResources = userMapper.getAllResources();
for (Resources resource : allResources) {
if(antPathMatcher.match(resource.getUrl(), requestURI)){
String[] permissions = resource.getPermissions().stream()
.map(Permission::getName).toArray(String[]::new);
//存入资源所需求的权限
return SecurityConfig.createList(permissions);
}
}
return null;
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class<?> clazz) {
return FilterInvocation.class.isAssignableFrom(clazz);
}
}
决策器,在前后端别离时,这是必须要增加的 承继AccessDecisionManager接口
在自定义权限元数据时,咱们现已将拜访受维护资源所需求的权限增加到ConfigAttribute中去了,所以在验证权限时,咱们只需求将ConfigAttribute中的权限与Authentication中保存的用户权限进行比较即可
@Component
public class CustomAccessDecisionManager implements AccessDecisionManager {
@Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
//假如权限元数据为空,则直接放行,即不需求权限就能拜访
if(CollUtil.isEmpty(configAttributes)){
return;
}
for (ConfigAttribute configAttribute : configAttributes) {
String attribute = configAttribute.getAttribute();
//取出用户具有的权限
for (GrantedAuthority authority : authentication.getAuthorities()) {
if (attribute.trim().equals(authority.getAuthority())){
return;
}
}
throw new AccessDeniedException("对不起,你没有权限");
}
}
@Override
public boolean supports(ConfigAttribute attribute) {
return true;
}
@Override
public boolean supports(Class<?> clazz) {
return true;
}
}
最后还要将决策器和元数据都注册到SpringSecurity的过滤器链中:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private UserService userService;
@Autowired
private LoginFilter loginFilter;
@Autowired
private CustomSecurityMetadataSource customSecurityMetadataSource;
@Autowired
private CustomAccessDecisionManager customAccessDecisionManager;
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
ApplicationContext applicationContext = http.getSharedObject(ApplicationContext.class);
return http.apply(new UrlAuthorizationConfigurer<>(applicationContext))
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O object) {
object.setSecurityMetadataSource(customSecurityMetadataSource);
object.setAccessDecisionManager(customAccessDecisionManager);
return object;
}
})
.and()
.authorizeRequests()
.antMatchers("/login", "/signUp", "/getPublicKey").permitAll()
.anyRequest().authenticated()
.and()
.cors()
.configurationSource(corsConfigurationSource())
.and()
.userDetailsService(userService)
.formLogin()
.loginPage("http://127.0.0.1:5173/")
.and()
.addFilterBefore(loginFilter, UsernamePasswordAuthenticationFilter.class)
.csrf().disable()
.build();
}
@Bean
SecurityFilterChain web(HttpSecurity http) throws Exception {
http.authorizeHttpRequests((authorize) -> authorize
.anyRequest().authenticated());
return http.build();
}
//跨域
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedMethod("*");
corsConfiguration.addAllowedOrigin("*");
corsConfiguration.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", corsConfiguration);
return source;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager() {
DaoAuthenticationProvider dao = new DaoAuthenticationProvider();
dao.setUserDetailsService(userService);
dao.setPasswordEncoder(passwordEncoder());
return new ProviderManager(dao);
}
}
以上便是这个项目的大致思路了,具体代码能够参考我的github仓库
项目中或许存在一些槽点,欢迎纠正。