目标
使用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
自动跳到登录页面
文章作者
duansheli
上次更新
2019-12-25
(325c7b3)