学习之路比较遥远,需要一点一点更新。
SpringSecurity的详细介绍就不说了,度娘会告诉你一切。但还是要多说一句:主要就是认证和授权。
光速搭建
其实吧就是创建项目勾一勾就行了,正常搭建一个springboot项目
这里web的勾一勾
这里security的勾一勾,完事!
我们启动项目并访问,会发现进入了security的内置界面:
用户名默认user 密码可以在控制台找到。
自写一下
其实上方的这种形式并不满足我们自己去做事情,你想搭建一个自己的登陆界面,五颜六色的那种,看着就要中病毒的花花的界面,唬唬人,这咋办呢?
那先自定义个鬼屎简单界面吧
定义一个登录界面login.html,注意input标签内的type不要用错了
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/login" method="post">
用户名:<input type="text" name="username"/><br/>
密码:<input type="password" name="password"/><br/>
<input type="submit" value="登录">
</form>
</body>
</html>
再来一个首页吧,登录成功的页面indexTest.html(这里讲述一个细节:当你命名为index.html时,一旦通过了登录验证会自动跳转到index.html,所以学习过程中,为了深入了解自定义,命名就换一个,不要用index.html命名)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
登录成功
</body>
</html>
定义好了,但是我不会用,咋办。咋能让security认识咱们的页面,用咱们的页面去做认证呢?
咱们先来分析一波
先认识几个Security的走向吧
正常来说,我们是从数据查用户的情况对吧?查用户名,有木有这个用户?然后匹配密码,密码输入的对不对?
我们先看security怎么做:
1.从UserDetailsService开始
public interface UserDetailsService {
//参数String var1,就是username 我们输入的用户名
//这里就是根据用户名去查信息,返回一个UserDetails对象
UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}
此时我们需要进一步去看看UserDetails有什么?
public interface UserDetails extends Serializable {
//权限
Collection<? extends GrantedAuthority> getAuthorities();
//密码
String getPassword();
//用户名
String getUsername();
//是否未过期
boolean isAccountNonExpired();
//是否未锁定
boolean isAccountNonLocked();
//用户认证(这里是指密码)是否未超期
boolean isCredentialsNonExpired();
//用户是否可用,如果被禁用不能登陆的
boolean isEnabled();
}
那么这个接口肯定不是我们要用的东西,我们要用的是他的实现类。我们看他的其中一个实现类——User类
//主要的属性
private String password;
private final String username;
private final Set<GrantedAuthority> authorities;
private final boolean accountNonExpired;
private final boolean accountNonLocked;
private final boolean credentialsNonExpired;
private final boolean enabled;
//两个构造方法
public User(String username, String password, Collection<? extends GrantedAuthority> authorities) {
//这里调用的就是下方的构造方法
this(username, password, true, true, true, true, authorities);
}
public User(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {
Assert.isTrue(username != null && !"".equals(username) && password != null, "Cannot pass null or empty values to constructor");
this.username = username;
this.password = password;
this.enabled = enabled;
this.accountNonExpired = accountNonExpired;
this.credentialsNonExpired = credentialsNonExpired;
this.accountNonLocked = accountNonLocked;
this.authorities = Collections.unmodifiableSet(sortAuthorities(authorities));
}
判断用户名是否为空,如果为空抛异常;不为空就赋值
实际的登录逻辑大致是这样的:
找UserDetailsService 中的loadUserByUsername方法。根据用户名查数据库看是否存在,如果存在了就比较一下密码,然后匹配正确,就构造一个User对象,赋上相应的用户名、密码、权限等等值。匹配不对就登录失败喽
2.认证密码
public interface PasswordEncoder {
//参数CharSequence var1,是指明文密码,方法就是对其加密
//这里它推荐使用“SHA-1”或者“哈希使用8位或者随机延续”的方式加密。
String encode(CharSequence var1);
//匹配,CharSequence var1 明文密码,String var2加密好的密码,进行匹配
boolean matches(CharSequence var1, String var2);
//对于加密过的密码进一步加密,默认返回false
default boolean upgradeEncoding(String encodedPassword) {
return false;
}
}
这里官方推荐使用其实现类——BCryptPasswordEncoder来进行加密和解密
这里有一个神奇的东西:
相同的明文通过BCryptPasswordEncoder加密的密文每次都是不一样的!
例如:对“123”进行加密,encode()后返回的String 是不同的。也就是你加密很多次都是不一样的密文。但是用这些不同的密文去进行解密,调用matches方法,传入“123”和不同的密文,结果都是返回true。
先上源码
所以是为啥相同明文加密结果密文都不一样,但解密又可以很ok呢?
1、首先聊加密:
他先获取一个随机数salt,然后根据salt通过某种规则获取real_salt,同时real_salt也是参与了加密。
随机数salt + "123" 进行加密 ——> “MMM”
本质,其实salt也并非随便随机,而是有一定规则,是salt+realsalt+"123"进行加密得到密文"MMM"
截图的源码中看
数字6上一行代码的rounds是与salt直接相关的
而数字6下一行代码的saltb是与realsalt直接相关的
后续的源码会用rounds+saltb内容进行加密
2、其次是解密:
原理是对传入的明文"123"和盐(这里源码中,这个matches方法中先加密的盐不再是随机数,用的就是数据库查出的密文“MMM”)进行加密后生成"xxx",再和数据库存储的密文“MMM”进行匹配。
查数据库拿到密文“MMM”
“123” + 盐(值为“MMM”) 进行加密 ——> “xxx”
再用“xxx” 和 “MMM”进行匹配,不是匹配值,是匹配他们的哈希值,相等就是匹配成功
总结
总结
总结
重要说三遍
所以,加密的过程中
第一次加密:由于用到了随机数salt(具有一定规则的随机数),用不确定值salt去保证得到的密码每次都是不一样的。同时又会根据salt的生成规范,得到一个固定的realsalt(解密要用),然后把salt和realsalt一起融入到密码中进行加密,生成密码。
第二次以及后续加密也会由于salt的不通,得到的密码不同。
解密:把传入的明文值(“如123”)进行加密,然后用数据库查到的密码密文作为salt,由于salt的规则,可以根据生成的密文按规则获取到realsalt,然后把传入的明文+salt一同加密后,得到的新密文与数据库密文进行哈希匹配。
简单自定义1开始
上述已经定义了两个简单页面——indexTest.html和login.html页面。
那么我们要进行自定义的效果:
1.认证前:进入我们自定义的login.html页面去认证,而非security默认登录页面
2.认证通过后:能进入其他页面,比如indexTest.html
3.认证失败后:跳转一个失败页面
好了,小目标明确。那我们要新增一个loginError.html页面,认证失败用。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
登录失败
<a href="/login.html">请重新登录</a>
</body>
</html>
下面开始走后端代码
1.实现UserDetailsService——自定义用户认证时使用
这里有一个困扰本人的问题,暂时未得到解决
问题:不加@Service,就走不到自定义的UserDetailServiceImpl中,那么security怎么做的?怎么识别到注入spring就走自定义的逻辑,不注入spring就走原来的默认逻辑。
但可以确定的是。不加@Service注入spring中,就无法走到自定义的逻辑中
下面正式开始:
我们自定义一个类UserDetailsServiceImpl实现UserDetailsService接口
注意:下方的UserData 是由于我懒得链接数据库,自制的一个伪数据。userEnity是用户实体
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String userName)
throws UsernameNotFoundException {
UserEnity userEnity = new UserEnity();
//1.根据用户名查用户存在性
userEnity = getUserData(userName, userEnity);
if (userEnity == null) {
throw new UsernameNotFoundException("用户不存在");
}
//2.比较密码(注册时已经加密,如果成功返回UserDetails),返回security的user类
return new User(userName, userEnity.getPassword(), AuthorityUtils.commaSeparatedStringToAuthorityList("admin.normal"));
}
private UserEnity getUserData(String userName, UserEnity userEnity) {
UserData userData = new UserData();
List<UserEnity> userList = userData.getUserList();
for (UserEnity userEnityTemp : userList) {
if (userEnityTemp.getUserName().equals(userName)) {
//数据赋值
userEnity = userEnityTemp;
return userEnity;
}
}
return null;
}
}
//下方为伪数据代码
@Data
public class UserData {
private List<UserEnity> userList = new ArrayList<>();
public UserData() {
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
UserEnity userEnity = new UserEnity();
userEnity.setUserName("admin");
userEnity.setPassword(bCryptPasswordEncoder.encode("admin123"));
this.userList.add(userEnity);
userEnity = new UserEnity();
userEnity.setUserName("xiaoming");
userEnity.setPassword(bCryptPasswordEncoder.encode("xiaoming123"));
this.userList.add(userEnity);
userEnity = new UserEnity();
userEnity.setUserName("boss");
userEnity.setPassword(bCryptPasswordEncoder.encode("boss123"));
this.userList.add(userEnity);
}
}
//下方为用户实体
@Data
public class UserEnity {
private String userName;
private String password;
}
2.自定义Security配置类——拦截或放行以及跳转页面用
那么我们开始定义这个配置类了
如下代码:
重点1:@Configuration
重点2:extends WebSecurityConfigurerAdapter
重点3:重写方法configure(HttpSecurity http)这个方法,有很多同名,看清选择
重点4:注释内容要读一下!!!!!
@Configuration
public class SecurityPasswordConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
//自定义登录页面
.loginPage("/login.html")
//自定义登录接口路径,和自定义页面login.html的action=路径要一致
//.loginProcessingUrl("/user/login")这个有必要解释一下。他其实就是抓取页面表单提交的路径去交给springSecurity做认证
//比如login.html页面的信息表单action="/a"提交,那么这里就要配置/a,抓取/a请求信息去交给SpringSecurity做认证
.loginProcessingUrl("/login")
//这个successForwardUrl直接跳转页面的话,本质将是一个GET请求,调用会报405错。
// 改为跳转路径,则本质为post请求
// .successForwardUrl("/indexTest.html") 改为/toIndex
.successForwardUrl("/toIndex")
//这里就顾名思义了,跳转失败的页面
.failureForwardUrl("/loginError")
;
//授权,如果不授权就可以随便进入,授权成功以后,页面不会拦截
/*
这里需要讲述一个逻辑:
1.登录login以后,跳转到成功界面,这个indexTest.html不需要放行,因为已经登录了
2.错误页面loginError.html需要放行,因为是登录前的操作
*/
http.authorizeRequests()
//放行,不需要认证的页面
.antMatchers("/login.html").permitAll()
.antMatchers("/loginError.html").permitAll()
.anyRequest().authenticated()
;
//关闭防火墙,csrf叫跨站请求伪造
http.csrf().disable();
}
@Bean
public PasswordEncoder getPasswordRule() {
return new BCryptPasswordEncoder();
}
}
此时已经可以启动一波测测看看了!
简单自定义2开始
接着简单自定义1,我们进一步探讨一些东西。
首先从login.html开始
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/login" method="post">
用户名:<input type="text" name="username"/><br/>
密码:<input type="password" name="password"/><br/>
<input type="submit" value="登录">
</form>
</body>
</html>
username这个参数会传递到哪里?security是怎么去获取这个东西的。
源码看起来费劲可以先看下边的思路对着看源码
上源码对象UsernamePasswordAuthenticationFilter.class:
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/login", "POST");
private String usernameParameter = "username";
private String passwordParameter = "password";
private boolean postOnly = true;
public UsernamePasswordAuthenticationFilter() {
super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
}
public UsernamePasswordAuthenticationFilter(AuthenticationManager authenticationManager) {
super(DEFAULT_ANT_PATH_REQUEST_MATCHER, authenticationManager);
}
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
} else {
String username = this.obtainUsername(request);
username = username != null ? username : "";
username = username.trim();
String password = this.obtainPassword(request);
password = password != null ? password : "";
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
this.setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
}
@Nullable
protected String obtainPassword(HttpServletRequest request) {
return request.getParameter(this.passwordParameter);
}
@Nullable
protected String obtainUsername(HttpServletRequest request) {
return request.getParameter(this.usernameParameter);
}
//后续代码省略...
}
思路:
1.源码中首先定义了属性:
private String usernameParameter = "username";
private String passwordParameter = "password";
2.认证逻辑的方法
attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
这个方法中是先判定请求类型是不是POST,不是就报错
然后调用 obtainUsername()方法,也就是获取定义的属性usernameParameter 的值
获取usernameParameter 干嘛?作为key从request中取value
下面问题来了!我想改一下这个参数名称,不想用username,我想驼峰命名或者叫godName(客户就是上帝)我页面传参用想用godName
这怎么办,他怎么识别?别慌,改!
1.改login.html
//先把用户名的name值改了
<form action="/login" method="post">
用户名:<input type="text" name="godName"/><br/>
密码:<input type="password" name="password"/><br/>
<input type="submit" value="登录">
</form>
然后改哪里?
2.改config类
@Configuration
public class SecurityPasswordConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.loginPage("/login.html")
//这里就是改造点,加个 .usernameParameter("godName")
.usernameParameter("godName")
.loginProcessingUrl("/login")
.successForwardUrl("/toIndex")
.failureForwardUrl("/loginError")
;
http.authorizeRequests()
.antMatchers("/login.html").permitAll()
.antMatchers("/loginError.html").permitAll()
.anyRequest().authenticated()
;
http.csrf().disable();
}
@Bean
public PasswordEncoder getPasswordRule() {
return new BCryptPasswordEncoder();
}
}
上述两处改完即可
这样子就实现了username参数名称的自定义。password也是一样的改造2点,可以照葫芦画瓢,自行进行一定的参悟和探索~
那我看源码是写死的username
private String usernameParameter = "username";
咋后台这里改了就可以呢??其实是这样的:
程序启动过程中
1.usernameParameter 属性的值会被赋值为username
2.调用配置类SecurityPasswordConfig 中新增的.usernameParameter(“godName”)方法
点进去可以看到下方美景:
public FormLoginConfigurer<H> usernameParameter(String usernameParameter) {
((UsernamePasswordAuthenticationFilter)this.getAuthenticationFilter()).setUsernameParameter(usernameParameter);
return this;
}
可以看到.usernameParameter(“godName”)其实是调用了UsernamePasswordAuthenticationFilter中的setUsernameParameter,也就是把属性usernameParameter的值替换为了“godName”,后边再从request中取value用的key将会是“godName”