目标

使用spring security添加两种登录方式

  • 手机号,验证码登录
  • 用户名 密码 图形验证码登录

运行环境

  • win10-x64
  • jdk-v1.8
  • springboot-v2.1.4
  • springsecurity-5.1.5

环境搭建

新建maven项目 配置pom.xml依赖项

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.4.RELEASE</version>
    <relativePath/>
</parent>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- 引入freemarker包 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-freemarker</artifactId>
    </dependency>
    <!-- 引入spring-security包 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-config</artifactId>
    </dependency>
</dependencies>

application.properties

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
server.port=8822
spring.application.name=hello-login
#debug=true
spring.freemarker.template-loader-path=classpath:/templates
spring.freemarker.cache=false
spring.freemarker.charset=UTF-8
spring.freemarker.check-template-location=true
spring.freemarker.content-type=text/html
spring.freemarker.expose-request-attributes=false
spring.freemarker.expose-session-attributes=false
spring.freemarker.request-context-attribute=request
spring.freemarker.suffix=.ftl

项目启动类 App_Main.java

1
2
3
4
5
6
7
8
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class App_Main {
    public static void main(String[] args) {
        SpringApplication.run(App_Main.class, args);
    }
}

spring security登录项

登录选项分两种:
用户名密码登录-/loginByCode
手机验证码登录-/loginByPwd

查询用户信息

用户登录的时候必然要去数据库查询用户信息
此处没有使用数据库, 直接代码写死了

Db

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import java.util.HashMap;
import java.util.Map;

@Component
public class Db {
    Map<String, UserDetails> map = new HashMap();

    public Db() {
        MyUserDetail u1 = new MyUserDetail();
        u1.setUsername("1381234001");
        u1.setPassword("123");
        u1.list.add(new MyRole("ROLE_pwd"));
        MyUserDetail u2 = new MyUserDetail();
        u2.setUsername("tom");
        u2.setPassword("123");
        u2.list.add(new MyRole("ROLE_phone"));

        map.put("1381234001", u1);
        map.put("tom", u2);
    }

    public UserDetails findUserByName(String userName) {
        return map.get(userName);
    }
}

MyRole

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import org.springframework.security.core.GrantedAuthority;
import org.springframework.util.Assert;

public class MyRole implements GrantedAuthority {
    private final String role;

    public MyRole(String role) {
        Assert.hasText(role, "A granted authority textual representation is required");
        this.role = role;
    }

    public String getAuthority() {
        return this.role;
    }
}

MyUserDetail

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import org.springframework.security.core.userdetails.UserDetails;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

public class MyUserDetail implements UserDetails {
    String username;
    String password;
    List<MyRole> list = new ArrayList<>();

    public void setUsername(String username) {
        this.username = username;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    @Override
    public Collection<MyRole> getAuthorities() {
        return list;
    }

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return username;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

MyUserDetailsService

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import fluffy.mo.loginDb.Db;
import org.springframework.beans.factory.annotation.Autowired;
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.Component;

@Component
public class MyUserDetailsService implements UserDetailsService {
   @Autowired
   Db db;
   @Override
   public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
      return db.findUserByName(userName);
   }
}

自定义异常

1
2
3
4
5
6
7
import org.springframework.security.core.AuthenticationException;

public class UnSupportAuthenticationException extends AuthenticationException {
    public UnSupportAuthenticationException(String explanation) {
        super(explanation);
    }
}

用户名密码登录

登录逻辑

  • 定义一个PwdAuthenticationToken
  • 针对此登录添加一个filter
    在filter中将登录参数封装成一个PwdAuthenticationToken
  • 定义一个AuthenticationProvider做两件事
    查询出该用户的用户信息
    核对用户信息

图形验证码

此处省略了图形验证码的功能
图形验证逻辑为:
检查用户是否存在
根据登录名tom 生成6位随机字符串846598
将其存入redis tom-846598
846598生成一个图片展示在前台
另外代码中要为该请求放行, 否则未登录的用户无法访问
http.antMatchers( “/imgCode”).permitAll()

PwdAuthenticationToken

1
2
3
4
5
6
7
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;

public class PhoneCodeAuthenticationToken extends UsernamePasswordAuthenticationToken {
    public PhoneCodeAuthenticationToken(String phone, String phoneCode) {
        super(phone, phoneCode);
    }
}

PhoneCodeAuthenticationFilter

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class PhoneCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

    public PhoneCodeAuthenticationFilter(String matchPath, AuthenticationManager authenticationManager) {
        super(new AntPathRequestMatcher(matchPath, "POST"));
        super.setAuthenticationManager(authenticationManager);
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse httpServletResponse) throws AuthenticationException, IOException, ServletException {
        if (!request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        } else {
            String phone = obtainUsername(request);
            String pwd = obtainPassword(request);

            UsernamePasswordAuthenticationToken authRequest = new PhoneCodeAuthenticationToken(phone,pwd);;
            return this.getAuthenticationManager().authenticate(authRequest);
        }
    }

    protected String obtainUsername(HttpServletRequest request) {
        Object obj = request.getParameter("uname");
        return null == obj ? "" : obj.toString().trim();
    }

    protected String obtainPassword(HttpServletRequest request) {
        Object obj = request.getParameter("pwd");
        return null == obj ? "" : obj.toString();
    }
}

PhoneCodeAuthenticationProvider

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import fluffy.mo.login.MyUserDetailsService;
import fluffy.mo.login.UnSupportAuthenticationException;
import fluffy.mo.loginDb.MyUserDetail;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

@Component
public class PhoneCodeAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
    @Autowired
    MyUserDetailsService userDetailsService;
    @Override
    protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        if (authentication instanceof PhoneCodeAuthenticationToken) {
            // 查询用户
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);
            return userDetails;
        } else {
            throw new UnSupportAuthenticationException("不支持的登录,仅支持手机验证码的登录");
        }
    }

    @Override
    protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        MyUserDetail detail = (MyUserDetail) userDetails;
        System.out.println("核对手机验证码。。。");
        // 去redis中查询验证码
        if(StringUtils.isEmpty("")){
            throw new UnSupportAuthenticationException("验证码不正确,请重新输入");
        }
        System.out.println("手机验证码正确");
        detail.setPassword(null);
    }
}

注册bean

该filter不是注册到servlet容器上
而是添加到到filterchain中 filterchain会以filter身份注册到容器上

1
2
3
http.addFilterBefore(new PhoneCodeAuthenticationFilter("/loginByCode", authenticationManager),UsernamePasswordAuthenticationFilter.class)
```java
该pwdAuthenticationProvider需要注册到authenticationManager中

auth.authenticationProvider(pwdAuthenticationProvider)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15

## 手机验证码登录
手机验证码登录的流程和用户名密码登录流程类似  
需要将生成的验证码保存到redis中 设置过期时间5分钟  
AuthenticationProvider在检查时,就去redis中查询该验证码  

### PhoneCodeAuthenticationToken
```java
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;

public class PhoneCodeAuthenticationToken extends UsernamePasswordAuthenticationToken {
    public PhoneCodeAuthenticationToken(String phone, String phoneCode) {
        super(phone, phoneCode);
    }
}

PhoneCodeAuthenticationFilter

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class PhoneCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

    public PhoneCodeAuthenticationFilter(String matchPath, AuthenticationManager authenticationManager) {
        super(new AntPathRequestMatcher(matchPath, "POST"));
        super.setAuthenticationManager(authenticationManager);
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse httpServletResponse) throws AuthenticationException, IOException, ServletException {
        if (!request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        } else {
            String phone = obtainUsername(request);
            String pwd = obtainPassword(request);

            UsernamePasswordAuthenticationToken authRequest = new PhoneCodeAuthenticationToken(phone,pwd);;
            return this.getAuthenticationManager().authenticate(authRequest);
        }
    }

    protected String obtainUsername(HttpServletRequest request) {
        Object obj = request.getParameter("uname");
        return null == obj ? "" : obj.toString().trim();
    }

    protected String obtainPassword(HttpServletRequest request) {
        Object obj = request.getParameter("pwd");
        return null == obj ? "" : obj.toString();
    }
}

PhoneCodeAuthenticationProvider

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import fluffy.mo.login.MyUserDetailsService;
import fluffy.mo.login.UnSupportAuthenticationException;
import fluffy.mo.loginDb.MyUserDetail;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

@Component
public class PhoneCodeAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
    @Autowired
    MyUserDetailsService userDetailsService;
    @Override
    protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        if (authentication instanceof PhoneCodeAuthenticationToken) {
            // 查询用户
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);
            return userDetails;
        } else {
            throw new UnSupportAuthenticationException("不支持的登录,仅支持手机验证码的登录");
        }
    }

    @Override
    protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        MyUserDetail detail = (MyUserDetail) userDetails;
        System.out.println("核对手机验证码。。。");
        // 去redis中查询验证码
        if(StringUtils.isEmpty("")){
            throw new UnSupportAuthenticationException("验证码不正确,请重新输入");
        }
        System.out.println("手机验证码正确");
        detail.setPassword(null);
    }
}

配置spring-secruity登录项

SecurityConfig

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import fluffy.mo.login.phonecode.PhoneCodeAuthenticationFilter;
import fluffy.mo.login.phonecode.PhoneCodeAuthenticationProvider;
import fluffy.mo.login.pwd.PwdAuthenticationFilter;
import fluffy.mo.login.pwd.PwdAuthenticationProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
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.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@EnableWebSecurity()
// 启用注解拦截
@EnableGlobalMethodSecurity(prePostEnabled=true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        AuthenticationManager authenticationManager = authenticationManager();
        http
                .addFilterBefore(new PhoneCodeAuthenticationFilter("/loginByCode", authenticationManager),UsernamePasswordAuthenticationFilter.class)
                .addFilterBefore(new PwdAuthenticationFilter("/loginByPwd", authenticationManager),UsernamePasswordAuthenticationFilter.class)
                .authorizeRequests()
                .antMatchers( "/login").permitAll()
                .antMatchers(HttpMethod.POST, "/loginBy*").permitAll()
                .antMatchers("/role/*").access("authenticated and @authService.canAccess(request,authentication)")
                .antMatchers("/**").authenticated()
//                .and().csrf().disable().cors()
                .and().formLogin().loginPage("/login")
//                .failureForwardUrl("/login")
                .and().logout().logoutUrl("/logout")
        ;
    }

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth, PhoneCodeAuthenticationProvider phoneProvider, PwdAuthenticationProvider pwdAuthenticationProvider) throws Exception {
        auth.authenticationProvider(phoneProvider)
                .authenticationProvider(pwdAuthenticationProvider);
    }

//    @Bean
//    @Override
//    public AuthenticationManager authenticationManagerBean() throws Exception {
//        return super.authenticationManagerBean();
//    }
}

AuthService

上一个文件SecurityConfig.java中以代码的形式, 声明了权限要求
同时也对”/role/*“声明了调用方法检查是否有权限调用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import fluffy.mo.loginDb.MyRole;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import java.util.*;
import java.util.stream.Collectors;

@Component
public class AuthService {
    Map<String, List> map = new HashMap<>();

    public AuthService() {
        map.put("/role/pwd",Arrays.asList("pwd","manager"));
        map.put("/role/pee",Arrays.asList("phone","admin"));
    }

    public boolean canAccess(HttpServletRequest request, Authentication authentication) {
        // 请求的uri
        String requestURI = request.getRequestURI();
        System.out.println(requestURI);
        Object principal = authentication.getPrincipal();
        if(principal == null || "anonymousUser".equals(principal.toString())){
            return false;
        }
        if(authentication instanceof UsernamePasswordAuthenticationToken){
        // 该uri对应的角色是什么
            List<String> list = map.get(requestURI);
            if(null != list){
                // 该用户是否有这个权限
                List<MyRole> authorities = (List<MyRole>) authentication.getAuthorities();
                List<String> collect = authorities.stream()
                        .map(x-> x.getAuthority().replace("ROLE_",""))
                        .collect(Collectors.toList());
                // 取交集
                collect.retainAll(list);
                if(collect.size() > 0){
                    return true;
                }
            }
            return false;
        }
        return true;
    }

}

访问安全信息

登录页面

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@Controller
public class LoginController {
    @RequestMapping("login")
    public String loginP() {
        return "login";
    }
    @RequestMapping("logout")
    public String logout(HttpServletRequest req) {
        req.getSession().invalidate();
        return "redirect:login";
    }
}

resources\templates\login.ftl

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登录</title>
</head>
<body>
<br>
<form id="loginForm" action="/loginByCode" method="post">
    <table>
        <tbody>
        <tr> <td id="m1">用户名</td><td><input name="uname"></td> </tr>
        <tr> <td id="m2">密码</td><td><input name="pwd"></td> </tr>
        <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
        <tr> <td><input type="submit" value="提交登录"></td> </tr>
        </tbody>
    </table>
</form>
<br>
<a href="#" onclick="toPwd()">用户名登录</a><br>
<a href="#" onclick="toPhone()">手机验证码登录</a>
</body>
<script>
    var loginForm = document.getElementById("loginForm");
    var m1 = document.getElementById("m1");
    var m2 = document.getElementById("m2");
    function toPwd() {
        loginForm.action="loginByPwd"
        m1.innerText="用户名"
        m2.innerText="密码"
    }
    function toPhone() {
        loginForm.action="loginByPhone"
        m1.innerText="手机号"
        m2.innerText="验证码"
    }
    toPwd();
</script>
</html>

HelloController

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {
    @RequestMapping("hello")
    public String hello() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        String name = authentication.getName();
        System.out.println("已登录用户-" + name);
        return "hello==" + name;
    }
    @RequestMapping("hello2")
    public String hello2() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        String name = authentication.getName();
        System.out.println("已登录用户-" + name);
        return "hello==" + name;
    }
    @RequestMapping("/changePwd/{username}")
    @PreAuthorize("#userId == authentication.principal.username or hasAuthority('admin')")
    public String changePassword(@PathVariable(value = "username") String userId) {
        System.out.println("修改密码-" + userId);
        return "success" ;
    }
    @RequestMapping("/role/{name}")
    public String hp(@PathVariable(value = "name") String name) {
        System.out.println("该用户-" + name);
        return "logined==" + name;
    }

}

测试

访问 http://127.0.0.1:8822/changePwd/tom
由于没登录会跳到登录页面 127.0.0.1:8822/login
用tom/123登录后
会跳到 http://127.0.0.1:8822/changePwd/tom
提示success
访问 http://127.0.0.1:8822/changePwd/peter
提示无权限

最后退出 http://127.0.0.1:8822/logout
访问 http://127.0.0.1:8822/changePwd/tom
自动跳到登录页面