Shiro与Spring Security都是主流的身份认证和权限控制安全框架,Shiro偏向于前后端不分离平台,而Spring Security更偏向于前后端分离平台。接下来简单列一下两种登录验证的执行流程和示例,了解实际运用中的登录执行流程,然后重点剖析一下密码验证的过程。其实,密码验证的本质就是比较用户输入的凭证(密码)和存储的凭证(加密后的密码)是否匹配 ,如果一致,则表示密码验证通过。
<dependency>
<groupId>org.springframework.securitygroupId>
<artifactId>spring-security-coreartifactId>
dependency>
在登录方法中通过UsernamePasswordAuthenticationToken把用户名、密码信息封装起来,使用AuthenticationManager的authenticate方法并传入UsernamePasswordAuthenticationToken对象
@RestController
public class LoginController {
@Resource
private AuthenticationManager authenticationManager;
@RequestMapping("/login")
public R login(String userName, String password){
Authentication authentication = null;
try {
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userName, password);
AuthenticationContextHolder.setContext(authenticationToken);
// 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
authentication = authenticationManager.authenticate(authenticationToken);
} catch (Exception e) {
e.printStackTrace();
return R.error();
}
//登录认证成功,生成token返回
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
String token = getToken(loginUser);
return R.ok().setData(token);
}
private String getToken(LoginUser loginUser) {
//TODO 按照自己的业务需求生成
return null;
}
}
上下文对象,用来存放身份认证信息
/**
* 上下文对象,用来存放身份认证信息
*/
public class AuthenticationContextHolder {
private static final ThreadLocal<Authentication> contextHolder = new ThreadLocal<>();
public static Authentication getContext() {
return contextHolder.get();
}
public static void setContext(Authentication context) {
contextHolder.set(context);
}
public static void clearContext() {
contextHolder.remove();
}
}
①同时定义UserDetailsServiceImpl类实现UserDetailsService,并重写loadUserByUsername方法,上面步骤就会执行到此方法中;
②在loadUserByUsername方法中通过上下文传值AuthenticationContextHolder获取到登录的用户名和密码;
③进行账号、密码验证
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserService userService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userService.selectUserByUserName(username);
boolean state = validate(user); //验证用户信息
if (!state) {
//账号、密码验证失败
throw new UsernameNotFoundException("账号、密码验证失败");
}
//生成UserDetails信息返回
UserDetails userDetails = new LoginUser();
return userDetails;
}
private boolean validate(User user){
Authentication usernamePasswordAuthenticationToken = AuthenticationContextHolder.getContext();
String password = usernamePasswordAuthenticationToken.getCredentials().toString();
//验证账号、密码
return new BCryptPasswordEncoder().matches(password, user.getPassword());
}
}
Spring Security主要使用BCryptPasswordEncoder进行加密的,BCryptPasswordEncoder用SHA-256+随机盐+密钥对密码进行加密,这种加密过程中不需要使用密钥,输入明文后由系统直接经过加密算法处理成密文,这种加密后的数据是无法被解密的,无法根据密文推算出明文。BCryptPasswordEncoder的加密过程中,同一个密码,由于盐是随机的,所以每次加密的结果都是不一样的,然后就是明文密码的hash值与数据库中存储密码hash值进行比较。
public static void main(String[] args) {
//初始化BCryptPasswordEncoder
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
//原始密码
String initPwd = "123456";
String encode1 = encoder.encode(initPwd);
System.out.println("1次加密结果:"+encode1);
String encode2 = encoder.encode(initPwd);
System.out.println("2次加密结果:"+encode2);
String encode3 = encoder.encode(initPwd);
System.out.println("3次加密结果:"+encode3);
boolean b1 = encoder.matches(initPwd, encode1);
System.out.println("1次加密认证:"+b1);
boolean b2 = encoder.matches(initPwd, encode2);
System.out.println("2次加密认证:"+b2);
boolean b3 = encoder.matches(initPwd, encode3);
System.out.println("3次加密认证:"+b3);
}
运行结果:
1次加密结果:$2a$10$8NfgZSeUG8mCApNeJumsg.Z6k3SF1HrGPB0FPuLDlFy2jYF.uHYAm
2次加密结果:$2a$10$.PTFbG0qwMHiQLgA5SY/pOOcHCUP7gZQvQYAEv6RRaE0R3Wc9e.hW
3次加密结果:$2a$10$YFI4vgXpnYlcfsqpplqJ1OjL8JczZSqYWjL0jIkmggTpe7cSuaaNi
1次加密认证:true
2次加密认证:true
3次加密认证:true
<dependency>
<groupId>org.apache.shirogroupId>
<artifactId>shiro-crypto-hashartifactId>
<version>1.6.0version>
dependency>
<dependency>
<groupId>org.apache.shirogroupId>
<artifactId>shiro-coreartifactId>
<version>1.6.0version>
dependency>
通过UsernamePasswordToken这个类会将用户登录信息封装起来,生成Token,然后通过SecurityUtils下的subject传入token执行登录方法
@RestController
public class LoginController {
@RequestMapping("/login")
public R login(String userName, String password){
UsernamePasswordToken token = new UsernamePasswordToken(userName, password);
Subject subject = SecurityUtils.getSubject();
try {
subject.login(token);
//验证通过,登录成功
return R.ok();
} catch (AuthenticationException e) {
e.printStackTrace();
//用户或密码错误,验证失败
return R.error();
}
}
}
①认证器会将Token分解开来,分成账号和密码,并通过Relam这个桥梁向数据库进行求证;
②然后自定义UserRealm类并继承AuthorizingRealm方法,重写doGetAuthenticationInfo方法中就可以从token中获取用户账号、密码信息;
③进行账号、密码验证
public class UserRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//授权方法 TODO
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//登录方法
UsernamePasswordToken upToken = (UsernamePasswordToken) authenticationToken;
String userName = upToken.getUsername();
String password = new String(upToken.getPassword());
//执行登录验证
User user = null;
try {
//验证账号、密码
user = validate(userName, password);
} catch (Exception e) {
throw new AuthenticationException(e.getMessage(), e);
}
return new SimpleAuthenticationInfo(user, password, getName());
}
private User validate(String userName, String password) throws AuthenticationException {
User user = userService.selectUserByUserName(userName);
if (user == null) {
throw new AuthenticationException("用户不存在");
}
//输入密码加密
String inputPwd = encryptPassword(userName, password, user.getSalt());
//数据库密码
String dbPwd = user.getPassword();
if (!inputPwd.equals(dbPwd)) {
throw new AuthenticationException("用户名、密码验证失败");
}
return user;
}
private static String encryptPassword(String userName, String password, String salt) {
return new Md5Hash(userName + password + salt).toHex().toString();
}
}
Shiro主要使用的是MD5进行加密的,底层就是传入登录名、密码、盐,使用MD5进行hash加密计算,得到一个密文字符串,通常会把盐和这个密文字符串存入到数据库中。下次用户登录的时候,传入登录名和密码,从数据库中获取到盐,MD5加密生成此时的加密密文,与数据库中存储的加密密文进行匹配。如果一致则验证通过,如果不一致,则验证不通过。
public static void main(String[] args) {
//密文密码
String dbPwd = "3d3e2e119996cedb7401025cced5c1b0";
//用户名
String userName = "admin";
//盐
String salt = "111111";
//明文密码
String inputPwd = "123456";
String encryptPassword = encryptPassword(userName, inputPwd, salt);
System.out.println("加密结果:"+encryptPassword);
boolean b = dbPwd.equals(encryptPassword);
System.out.println("认证结果:"+b);
}
public static String encryptPassword(String userName, String password, String salt) {
return new Md5Hash(userName + password + salt).toHex().toString();
}
运行结果:
加密结果:3d3e2e119996cedb7401025cced5c1b0
认证结果:true