SpringSecurity的旅途(喜欢的话,可以点个赞哦~)

学习之路比较遥远,需要一点一点更新。
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”

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值