Closed peteryuanpan closed 3 years ago
按照文档中的搭建步骤,由于以前有了经验,比较顺利
搭建步骤
Windows环境部署
Windows环境搭建请参考:mall在Windows环境下的部署;
注意:只启动mall-admin,仅需安装Mysql、Redis即可;
克隆mall-admin-web项目,并导入到IDEA中完成编译:前端项目地址;
mall-admin-web项目的安装及部署请参考:mall前端项目的安装与部署。
mall-admin
本项目的 SecurityConfig 中 configure(HttpSecurity httpSecurity) 是采用静态白名单配置的方法
SecurityConfig
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired(required = false)
private DynamicSecurityService dynamicSecurityService;
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = httpSecurity
.authorizeRequests();
//不需要保护的资源路径允许访问
for (String url : ignoreUrlsConfig().getUrls()) {
registry.antMatchers(url).permitAll();
}
...
}
@Bean
public IgnoreUrlsConfig ignoreUrlsConfig() {
return new IgnoreUrlsConfig();
}
...
}
IgnoreUrlsConfig
package com.macro.mall.security.config;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.ArrayList;
import java.util.List;
/**
* 用于配置白名单资源路径
* Created by macro on 2018/11/5.
*/
@Getter
@Setter
@ConfigurationProperties(prefix = "secure.ignored")
public class IgnoreUrlsConfig {
private List<String> urls = new ArrayList<>();
public List<String> getUrls() {
return urls;
}
}
application.yml
secure:
ignored:
urls: #安全路径白名单
- /swagger-ui.html
- /swagger-resources/**
- /swagger/**
- /**/v2/api-docs
- /**/*.js
- /**/*.css
- /**/*.png
- /**/*.ico
- /webjars/springfox-swagger-ui/**
- /actuator/**
- /druid/**
- /admin/login
- /admin/register
- /admin/info
- /admin/logout
- /minio/upload
一站式登陆功能分析(一)
登陆后,前端给后端发请求,后端是如何识别不同的用户,并进行处理的呢?
带着这个问题,我看了下代码,发现了JWT登录支持技术,它的github是:https://github.com/jwtk/jjwt
这个问题分别几个点
下面我们来分析一下项目代码
@Controller
@Api(tags = "UmsAdminController", description = "后台用户管理")
@RequestMapping("/admin")
public class UmsAdminController {
@Autowired
private UmsAdminService adminService;
@ApiOperation(value = "登录以后返回token")
@RequestMapping(value = "/login", method = RequestMethod.POST)
@ResponseBody
public CommonResult login(@Validated @RequestBody UmsAdminLoginParam umsAdminLoginParam) {
String token = adminService.login(umsAdminLoginParam.getUsername(), umsAdminLoginParam.getPassword());
if (token == null) {
return CommonResult.validateFailed("用户名或密码错误");
}
Map<String, String> tokenMap = new HashMap<>();
tokenMap.put("token", token);
tokenMap.put("tokenHead", tokenHead);
return CommonResult.success(tokenMap);
}
}
上面是 /login 接口的 Controller 代码,可以看出请求参数中是包含 username 的
UmsAdminController 类有一个属性 UmsAdminService adminService; 这个属性的具体实现如下
@Service
public class UmsAdminServiceImpl implements UmsAdminService {
@Override
public String login(String username, String password) {
String token = null;
//密码需要客户端加密后传递
try {
UserDetails userDetails = loadUserByUsername(username);
if(!passwordEncoder.matches(password,userDetails.getPassword())){
Asserts.fail("密码不正确");
}
if(!userDetails.isEnabled()){
Asserts.fail("帐号已被禁用");
}
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
token = jwtTokenUtil.generateToken(userDetails);
// updateLoginTimeByUsername(username);
insertLoginLog(username);
} catch (AuthenticationException e) {
LOGGER.warn("登录异常:{}", e.getMessage());
}
return token;
}
}
上面这里有一句话:token = jwtTokenUtil.generateToken(userDetails); 表示生成 token,详细代码如下
public class JwtTokenUtil {
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername());
claims.put(CLAIM_KEY_CREATED, new Date());
return generateToken(claims);
}
private String generateToken(Map<String, Object> claims) {
return Jwts.builder()
.setClaims(claims)
.setExpiration(generateExpirationDate())
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
}
可以看出上面的代码中有一句 userDetails.getUsername(),即 token 中是包含 username 的,也可以反向解析出来
上面这里就用到了 Jwts 了,github 的 quickStart https://github.com/jwtk/jjwt#quickstart 中也是这么建议实现的,如下
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import java.security.Key;
// We need a signing key, so we'll create one just for this example. Usually
// the key would be read from your application configuration instead.
Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
String jws = Jwts.builder().setSubject("Joe").signWith(key).compact();
好,到这里可以小结一下,UmsAdminController 中 login 方法负责处理 /login 接口,参数是 username、password,我们需要返回一个 tokenMap,存储的是 json 类型,其中参数有 token、tokenHead
来看一下前端的接口参数,可以看出 Response 有 token 了,token=A.B.C 是 JWT token 的形式
Request
Request URL: http://localhost:8080/admin/login
Request Method: POST
Status Code: 200
Remote Address: [::1]:8080
{"username":"admin","password":"macro123"}
Response
{"code":200,"message":"操作成功","data":{"tokenHead":"Bearer ","token":"eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhZG1pbiIsImNyZWF0ZWQiOjE2MDU2ODcwMzQwMjAsImV4cCI6MTYwNjI5MTgzNH0.FnrIMdNbp0kmqhwgQIvwIvRvzIbSOASEvj1vyADGZdsBsM-JKB6DmuzyP-tLPFnQXgmsh_X4MjU-HAmC-RcSkg"}}
后端的处理有多个操作
以切面类来说
@Aspect
@Component
@Order(1)
public class WebLogAspect {
@Pointcut("execution(public * com.macro.mall.controller.*.*(..))||execution(public * com.macro.mall.*.controller.*.*(..))")
public void webLog() {
}
@Around("webLog()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
//获取当前请求对象
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
//记录请求信息(通过Logstash传入Elasticsearch)
WebLog webLog = new WebLog();
Object result = joinPoint.proceed();
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod();
if (method.isAnnotationPresent(ApiOperation.class)) {
ApiOperation log = method.getAnnotation(ApiOperation.class);
webLog.setDescription(log.value());
}
long endTime = System.currentTimeMillis();
String urlStr = request.getRequestURL().toString();
webLog.setBasePath(StrUtil.removeSuffix(urlStr, URLUtil.url(urlStr).getPath()));
webLog.setIp(request.getRemoteUser());
webLog.setMethod(request.getMethod());
webLog.setParameter(getParameter(method, joinPoint.getArgs()));
webLog.setResult(result);
webLog.setSpendTime((int) (endTime - startTime));
webLog.setStartTime(startTime);
webLog.setUri(request.getRequestURI());
webLog.setUrl(request.getRequestURL().toString());
Map<String,Object> logMap = new HashMap<>();
logMap.put("url",webLog.getUrl());
logMap.put("method",webLog.getMethod());
logMap.put("parameter",webLog.getParameter());
logMap.put("spendTime",webLog.getSpendTime());
logMap.put("description",webLog.getDescription());
// LOGGER.info("{}", JSONUtil.parse(webLog));
LOGGER.info(Markers.appendEntries(logMap), JSONUtil.parse(webLog).toString());
return result;
}
}
这个切面类是针对所有 Controller(execution(public com.macro.mall..controller..(..)))切面,打印其日志
打印的日志例子如下
2020-11-18 12:15:04.302 INFO 10156 --- [nio-8080-exec-1] com.macro.mall.common.log.WebLogAspect : {"method":"GET","ip":"admin","description":"根据品牌名称分页获取品牌列表","uri":"/brand/list","url":"http://localhost:8080/brand/list","result":{"code":200,"data":{"totalPage":1,"pageSize":100,"list":[{"productCommentCount":100,"showStatus":1,"sort":500,"productCount":100,"factoryStatus":1,"name":"小米","bigPic":"http://macro-oss.oss-cn-shenzhen.aliyuncs.com/mall/images/20180518/5afd7778Nf7800b75.jpg","logo":"http://macro-oss.oss-cn-shenzhen.aliyuncs.com/mall/images/20180518/5a912944N474afb7a.png","id":6,"firstLetter":"M"},...
日志中有 "ip":"admin",它就是登陆的 username,而切面类中设置它的代码是 webLog.setIp(request.getRemoteUser());
debug step into 发现,request 实例是 SecurityContextHolderAwareRequestWrapper,而 getRemoteUser方法如下
public class SecurityContextHolderAwareRequestWrapper extends HttpServletRequestWrapper
@Override
public String getRemoteUser() {
Authentication auth = getAuthentication();
if ((auth == null) || (auth.getPrincipal() == null)) {
return null;
}
if (auth.getPrincipal() instanceof UserDetails) {
return ((UserDetails) auth.getPrincipal()).getUsername();
}
return auth.getPrincipal().toString();
}
}
其中有一个 Authentication auth = getAuthentication(); 是通过 ((UserDetails) auth.getPrincipal()).getUsername(); 获取 username
再来看一下JWT登录授权过滤器
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = httpSecurity
.authorizeRequests();
//不需要保护的资源路径允许访问
for (String url : ignoreUrlsConfig().getUrls()) {
registry.antMatchers(url).permitAll();
}
//允许跨域请求的OPTIONS请求
registry.antMatchers(HttpMethod.OPTIONS)
.permitAll();
// 任何请求需要身份认证
registry.and()
.authorizeRequests()
.anyRequest()
.authenticated()
// 关闭跨站请求防护及不使用session
.and()
.csrf()
.disable()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
// 自定义权限拒绝处理类
.and()
.exceptionHandling()
.accessDeniedHandler(restfulAccessDeniedHandler())
.authenticationEntryPoint(restAuthenticationEntryPoint())
// 自定义权限拦截器JWT过滤器
.and()
.addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
//有动态权限配置时添加动态权限校验过滤器
if (dynamicSecurityService != null) {
registry.and().addFilterBefore(dynamicSecurityFilter(), FilterSecurityInterceptor.class);
}
}
@Bean
public JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter() {
return new JwtAuthenticationTokenFilter();
}
}
SecurityConfig 中定义了一个 filter jwtAuthenticationTokenFilter
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
private static final Logger LOGGER = LoggerFactory.getLogger(JwtAuthenticationTokenFilter.class);
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Value("${jwt.tokenHeader}")
private String tokenHeader;
@Value("${jwt.tokenHead}")
private String tokenHead;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
String authHeader = request.getHeader(this.tokenHeader);
if (authHeader != null && authHeader.startsWith(this.tokenHead)) {
String authToken = authHeader.substring(this.tokenHead.length());// The part after "Bearer "
LOGGER.debug("authToken:{}", authToken);
String username = jwtTokenUtil.getUserNameFromToken(authToken);
LOGGER.info("checking username:{}", username);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
if (jwtTokenUtil.validateToken(authToken, userDetails)) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
LOGGER.debug(authentication.toString());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
LOGGER.info("authenticated user:{}", username);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
}
chain.doFilter(request, response);
}
}
public class JwtTokenUtil {
/**
* 从token中获取登录用户名
*/
public String getUserNameFromToken(String token) {
String username;
try {
Claims claims = getClaimsFromToken(token);
username = claims.getSubject();
} catch (Exception e) {
username = null;
}
return username;
}
/**
* 从token中获取JWT中的负载
*/
private Claims getClaimsFromToken(String token) {
Claims claims = null;
try {
claims = Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
} catch (Exception e) {
LOGGER.info("JWT格式验证失败:{}", token);
}
return claims;
}
}
可以看到上面有下面4句话
String username = jwtTokenUtil.getUserNameFromToken(authToken);
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
是不是和 SecurityContextHolderAwareRequestWrapper 中的 Authentication auth = getAuthentication();、((UserDetails) auth.getPrincipal()).getUsername(); 有什么关联性?
我尝试加了些日志,把多个 authentication 内存地址都打印出来
2020-11-18 17:33:13.116 DEBUG 15712 --- [nio-8080-exec-9] c.m.m.s.c.JwtAuthenticationTokenFilter : org.springframework.security.authentication.UsernamePasswordAuthenticationToken@b371c6f3: Principal: com.macro.mall.bo.AdminUserDetails@cb89c97; Credentials: [PROTECTED]; Authenticated: true; Details: null; Granted Authorities: 1:商品品牌管理, ...
2020-11-18 17:33:13.117 DEBUG 15712 --- [nio-8080-exec-9] c.m.m.s.component.DynamicSecurityFilter : Previously Authenticated: org.springframework.security.authentication.UsernamePasswordAuthenticationToken@b371538d: Principal: com.macro.mall.bo.AdminUserDetails@cb89c97; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@957e: RemoteIpAddress: 127.0.0.1; SessionId: null; Granted Authorities: 1:商品品牌管理, ...
2020-11-18 17:33:13.206 DEBUG 15712 --- [nio-8080-exec-9] com.macro.mall.common.log.WebLogAspect : org.springframework.security.authentication.UsernamePasswordAuthenticationToken@b371538d: Principal: com.macro.mall.bo.AdminUserDetails@cb89c97; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@957e: RemoteIpAddress: 127.0.0.1; SessionId: null; Granted Authorities: 1:商品品牌管理...
可以发现,DynamicSecurityFilter 与 WebLogAspect 的 UsernamePasswordAuthenticationToken 内存地址都是 b371538d,JwtAuthenticationTokenFilter 的 UsernamePasswordAuthenticationToken 内存地址是 b371c6f3,三者的 AdminUserDetails 内存地址都是 b371538d
因此我可以猜测一个结论:webLog.setIp(request.getRemoteUser()); 中的 remoteUser,是通过 JwtAuthenticationTokenFilter 或者 DynamicSecurityFilter 中设置的,具体方法大概是 将 username 传入 UserDetails,再将 UserDetails 传入 UsernamePasswordAuthenticationToken,而在 request.getRemoteUser() 中,可通过 Authentication auth = getAuthentication();、((UserDetails) auth.getPrincipal()).getUsername(); 的方式获取到 username
一站式登陆功能分析(二)
现在来看下前端
user.js
import { login, logout, getInfo } from '@/api/login'
import { getToken, setToken, removeToken } from '@/utils/auth'
const user = {
state: {
token: getToken(),
name: '',
avatar: '',
roles: []
},
mutations: {
SET_TOKEN: (state, token) => {
state.token = token
},
SET_NAME: (state, name) => {
state.name = name
},
SET_AVATAR: (state, avatar) => {
state.avatar = avatar
},
SET_ROLES: (state, roles) => {
state.roles = roles
}
},
actions: {
// 登录
Login({ commit }, userInfo) {
const username = userInfo.username.trim()
return new Promise((resolve, reject) => {
login(username, userInfo.password).then(response => {
const data = response.data
const tokenStr = data.tokenHead+data.token
setToken(tokenStr)
commit('SET_TOKEN', tokenStr)
resolve()
}).catch(error => {
reject(error)
})
})
},
// 获取用户信息
GetInfo({ commit, state }) {
return new Promise((resolve, reject) => {
getInfo().then(response => {
const data = response.data
if (data.roles && data.roles.length > 0) { // 验证返回的roles是否是一个非空数组
commit('SET_ROLES', data.roles)
} else {
reject('getInfo: roles must be a non-null array !')
}
commit('SET_NAME', data.username)
commit('SET_AVATAR', data.icon)
resolve(response)
}).catch(error => {
reject(error)
})
})
},
// 登出
LogOut({ commit, state }) {
return new Promise((resolve, reject) => {
logout(state.token).then(() => {
commit('SET_TOKEN', '')
commit('SET_ROLES', [])
removeToken()
resolve()
}).catch(error => {
reject(error)
})
})
},
// 前端 登出
FedLogOut({ commit }) {
return new Promise(resolve => {
commit('SET_TOKEN', '')
removeToken()
resolve()
})
}
}
}
export default user
login.js
import request from '@/utils/request'
export function login(username, password) {
return request({
url: '/admin/login',
method: 'post',
data: {
username,
password
}
})
}
auth.js
import Cookies from 'js-cookie'
const TokenKey = 'loginToken'
export function getToken() {
return Cookies.get(TokenKey)
}
export function setToken(token) {
return Cookies.set(TokenKey, token)
}
export function removeToken() {
return Cookies.remove(TokenKey)
}
menu.js
import request from '@/utils/request'
export function fetchList(parentId, params) {
return request({
url: '/menu/list/' + parentId,
method: 'get',
params: params
})
}
export function deleteMenu(id) {
return request({
url: '/menu/delete/' + id,
method: 'post'
})
}
user.js 负责登陆、登出等功能,它会提交到 login.js 中的 login 函数,注意到 user.js 的 Login 中有一个 setToken,LogOut 中有一个 removeToken,分别对应 auth.js 的代码,都调用了 Cookies
还注意到 menu.js 这样的业务请求(非login请求)中,根本没有传 token 参数
因此猜测:在 user.js 的 Login 中 setToken 后,将 token 存于 Cookies 中
抓了下前端的请求参数,如下,其中有 Authorization
Request
Request URL: http://localhost:8080/admin/info
Request Method: GET
Status Code: 200
Remote Address: [::1]:8080
Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhZG1pbiIsImNyZWF0ZWQiOjE2MDU3MDA5MTY5MzcsImV4cCI6MTYwNjMwNTcxNn0.W88TxAlTINrjuEt2Ofp43FKR4X8N_xAQ7FjcOOrXVCCtvlfQ9hnIR_0BaxYjNxOAi-vZAdn5GQUnh7W3tpxnig
打开chrome查看也能对应上,如下,把 Application.Cookies clear掉,再刷新界面,会回到 loginPage,登录后,Application.Cookies 中又有值了,也是如下
思考一下:前端在 login 接口返回成功后,是如何跳转到主页面的?
结论是:前端进行的跳转(而非后端302跳转)
下面是 /login 接口,可以看出返回的状态码是200,而不是302,而 https://github.com/peteryuanpan/notebook/issues/143#issuecomment-730122206 中我是通过 302跳转来实现的,逻辑不一样
UmsAdminController
@Controller
@Api(tags = "UmsAdminController", description = "后台用户管理")
@RequestMapping("/admin")
public class UmsAdminController {
@ApiOperation(value = "登录以后返回token")
@RequestMapping(value = "/login", method = RequestMethod.POST)
@ResponseBody
public CommonResult login(@Validated @RequestBody UmsAdminLoginParam umsAdminLoginParam) {
String token = adminService.login(umsAdminLoginParam.getUsername(), umsAdminLoginParam.getPassword());
if (token == null) {
return CommonResult.validateFailed("用户名或密码错误");
}
Map<String, String> tokenMap = new HashMap<>();
tokenMap.put("token", token);
tokenMap.put("tokenHead", tokenHead);
return CommonResult.success(tokenMap);
}
}
CommonResult
public class CommonResult<T> {
/**
* 状态码
*/
private long code;
/**
* 提示信息
*/
private String message;
public static <T> CommonResult<T> success(T data) {
return new CommonResult<T>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage(), data);
}
}
ResultCode
public enum ResultCode implements IErrorCode {
SUCCESS(200, "操作成功"),
FAILED(500, "操作失败"),
VALIDATE_FAILED(404, "参数检验失败"),
UNAUTHORIZED(401, "暂未登录或token已经过期"),
FORBIDDEN(403, "没有相关权限");
private long code;
private String message;
}
再尝试找一下前端的代码,看到 handleLogin 中有一句话 this.$router.push({path: '/'}),猜测是在登陆成功后,默认跳转到 /,也就是主页面
index.vue
<el-form-item prop="password">
<el-input name="password"
:type="pwdType"
@keyup.enter.native="handleLogin"
v-model="loginForm.password"
autoComplete="on"
placeholder="请输入密码">
<span slot="prefix">
<svg-icon icon-class="password" class="color-main"></svg-icon>
</span>
<span slot="suffix" @click="showPwd">
<svg-icon icon-class="eye" class="color-main"></svg-icon>
</span>
</el-input>
</el-form-item>
<el-form-item style="margin-bottom: 60px;text-align: center">
<el-button style="width: 45%" type="primary" :loading="loading" @click.native.prevent="handleLogin">
登录
</el-button>
<el-button style="width: 45%" type="primary" @click.native.prevent="handleTry">
获取体验账号
</el-button>
</el-form-item>
...
handleLogin() {
this.$refs.loginForm.validate(valid => {
if (valid) {
// let isSupport = getSupport();
// if(isSupport===undefined||isSupport==null){
// this.dialogVisible =true;
// return;
// }
this.loading = true;
this.$store.dispatch('Login', this.loginForm).then(() => {
this.loading = false;
setCookie("username",this.loginForm.username,15);
setCookie("password",this.loginForm.password,15);
this.$router.push({path: '/'})
}).catch(() => {
this.loading = false
})
} else {
console.log('参数验证不合法!');
return false
}
})
},
同时找到下面这一段代码,也体现了类似的逻辑,先通过 getToken() 获取 token,也就是 Application.Cookies 中 loginToken, 若获取不到,根据白名单进行判断,若获取得到,path 是 /login 的话 则下一步跳转到 /,否则根据 用户的权限生成可访问的路由表并动态添加
permission.js
import router from './router'
import store from './store'
import NProgress from 'nprogress' // Progress 进度条
import 'nprogress/nprogress.css'// Progress 进度条样式
import { Message } from 'element-ui'
import { getToken } from '@/utils/auth' // 验权
const whiteList = ['/login'] // 不重定向白名单
router.beforeEach((to, from, next) => {
NProgress.start()
if (getToken()) {
if (to.path === '/login') {
next({ path: '/' })
NProgress.done() // if current page is dashboard will not trigger afterEach hook, so manually handle it
} else {
if (store.getters.roles.length === 0) {
store.dispatch('GetInfo').then(res => { // 拉取用户信息
let menus=res.data.menus;
let username=res.data.username;
store.dispatch('GenerateRoutes', { menus,username }).then(() => { // 生成可访问的路由表
router.addRoutes(store.getters.addRouters); // 动态添加可访问路由表
next({ ...to, replace: true })
})
}).catch((err) => {
store.dispatch('FedLogOut').then(() => {
Message.error(err || 'Verification failed, please login again')
next({ path: '/' })
})
})
} else {
next()
}
}
} else {
if (whiteList.indexOf(to.path) !== -1) {
next()
} else {
next('/login')
NProgress.done()
}
}
})
router.afterEach(() => {
NProgress.done() // 结束Progress
})
同样的,打开chrome查看也能对应上,未登陆或退出登陆时,Application.Cookies 中会没有 loginToken,登陆后会出现 loginToken
来看下Controller + Mybatis + Mysql
getAdminInfo,对应 /admin/info 接口,这里就大有文章
@Controller
@Api(tags = "UmsAdminController", description = "后台用户管理")
@RequestMapping("/admin")
public class UmsAdminController {
@ApiOperation(value = "获取当前登录用户信息")
@RequestMapping(value = "/info", method = RequestMethod.GET)
@ResponseBody
public CommonResult getAdminInfo(Principal principal) {
if (principal == null) {
return CommonResult.unauthorized(null);
}
String username = principal.getName();
UmsAdmin umsAdmin = adminService.getAdminByUsername(username);
Map<String, Object> data = new HashMap<>();
data.put("username", umsAdmin.getUsername());
data.put("menus", roleService.getMenuList(umsAdmin.getId()));
data.put("icon", umsAdmin.getIcon());
List<UmsRole> roleList = adminService.getRoleList(umsAdmin.getId());
if (CollUtil.isNotEmpty(roleList)) {
List<String> roles = roleList.stream().map(UmsRole::getName).collect(Collectors.toList());
data.put("roles", roles);
}
return CommonResult.success(data);
}
}
UmsAdmin umsAdmin = adminService.getAdminByUsername(username); 这句话对应着 UmsAdminServiceImpl
public class UmsAdminServiceImpl implements UmsAdminService {
@Override
public UmsAdmin getAdminByUsername(String username) {
UmsAdmin admin = adminCacheService.getAdmin(username);
if (admin != null)
return admin;
UmsAdminExample example = new UmsAdminExample();
example.createCriteria().andUsernameEqualTo(username);
List<UmsAdmin> adminList = adminMapper.selectByExample(example);
if (adminList != null && adminList.size() > 0) {
admin = adminList.get(0);
adminCacheService.setAdmin(admin);
return admin;
}
return null;
}
}
List
UmsAdminMapper是一个Mybatis的Mapper类
public interface UmsAdminMapper {
long countByExample(UmsAdminExample example);
int deleteByExample(UmsAdminExample example);
int deleteByPrimaryKey(Long id);
int insert(UmsAdmin record);
int insertSelective(UmsAdmin record);
List<UmsAdmin> selectByExample(UmsAdminExample example);
UmsAdmin selectByPrimaryKey(Long id);
int updateByExampleSelective(@Param("record") UmsAdmin record, @Param("example") UmsAdminExample example);
int updateByExample(@Param("record") UmsAdmin record, @Param("example") UmsAdminExample example);
int updateByPrimaryKeySelective(UmsAdmin record);
int updateByPrimaryKey(UmsAdmin record);
}
以 selectByExample 举例,对应的部分xml如下
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.macro.mall.mapper.UmsAdminMapper">
<resultMap id="BaseResultMap" type="com.macro.mall.model.UmsAdmin">
<id column="id" jdbcType="BIGINT" property="id" />
<result column="username" jdbcType="VARCHAR" property="username" />
<result column="password" jdbcType="VARCHAR" property="password" />
<result column="icon" jdbcType="VARCHAR" property="icon" />
<result column="email" jdbcType="VARCHAR" property="email" />
<result column="nick_name" jdbcType="VARCHAR" property="nickName" />
<result column="note" jdbcType="VARCHAR" property="note" />
<result column="create_time" jdbcType="TIMESTAMP" property="createTime" />
<result column="login_time" jdbcType="TIMESTAMP" property="loginTime" />
<result column="status" jdbcType="INTEGER" property="status" />
</resultMap>
<select id="selectByExample" parameterType="com.macro.mall.model.UmsAdminExample" resultMap="BaseResultMap">
select
<if test="distinct">
distinct
</if>
<include refid="Base_Column_List" />
from ums_admin
<if test="_parameter != null">
<include refid="Example_Where_Clause" />
</if>
<if test="orderByClause != null">
order by ${orderByClause}
</if>
</select>
</mapper>
这是在从数据库中sql数据,用MySQLWorkbench,SELECT一下,数据结果如下
Mybatis帮忙从数据库中获取用户数据信息,并且生成一个 List
在application-dev.yml如下
spring:
datasource:
url: jdbc:mysql://localhost:3306/mall?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: root
password: 123456
druid:
initial-size: 5 #连接池初始化大小
min-idle: 10 #最小空闲连接数
max-active: 20 #最大连接数
web-stat-filter:
exclusions: "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*" #不统计这些请求数据
stat-view-servlet: #访问监控网页的登录用户名和密码
login-username: druid
login-password: druid
按照 http://www.macrozheng.com/#/deploy/mall_swarm_deploy_windows 部署 mall-swarm 项目
Nacos
SpringBootAdmin
Mall后台系统
mall-swarm 与 mall 项目不一样,它是基于 SpringCloud 的,所有请求先需要到 mall-gateway 中过滤,因此所有请求都以 http://localhost:8201/x/y 开头,请求端口固定是8201(可以通过配置文件修改),x 是 mall-auth、mall-admin、mall-portal 等子模块
mall-gateway 的 application,yml
server:
port: 8201
spring:
cloud:
gateway:
discovery:
locator:
enabled: true
lower-case-service-id: true #使用小写service-id
routes: #配置路由路径
- id: mall-auth
uri: lb://mall-auth
predicates:
- Path=/mall-auth/**
filters:
- StripPrefix=1
- id: mall-admin
uri: lb://mall-admin
predicates:
- Path=/mall-admin/**
filters:
- StripPrefix=1
- id: mall-portal
uri: lb://mall-portal
predicates:
- Path=/mall-portal/**
filters:
- StripPrefix=1
- id: mall-search
uri: lb://mall-search
predicates:
- Path=/mall-search/**
filters:
- StripPrefix=1
- id: mall-demo
uri: lb://mall-demo
predicates:
- Path=/mall-demo/**
filters:
- StripPrefix=1
security:
oauth2:
resourceserver:
jwt:
jwk-set-uri: 'http://localhost:8201/mall-auth/rsa/publicKey' #配置RSA的公钥访问地址
redis:
database: 0
port: 6379
host: localhost
password:
secure:
ignore:
urls: #配置白名单路径
- "/doc.html"
- "/swagger-resources/**"
- "/swagger/**"
- "/**/v2/api-docs"
- "/**/*.js"
- "/**/*.css"
- "/**/*.png"
- "/**/*.ico"
- "/webjars/springfox-swagger-ui/**"
- "/actuator/**"
- "/mall-auth/oauth/token"
- "/mall-auth/rsa/publicKey"
- "/mall-search/**"
- "/mall-portal/sso/login"
- "/mall-portal/sso/register"
- "/mall-portal/sso/getAuthCode"
- "/mall-portal/home/**"
- "/mall-portal/product/**"
- "/mall-portal/brand/**"
- "/mall-admin/admin/login"
- "/mall-admin/admin/register"
- "/mall-admin/minio/upload"
management: #开启SpringBoot Admin的监控
endpoints:
web:
exposure:
include: '*'
endpoint:
health:
show-details: always
mall-gateway 中 ResourceServerConfig 有 OAuth 的配置
package com.macro.mall.config;
import cn.hutool.core.util.ArrayUtil;
import com.macro.mall.authorization.AuthorizationManager;
import com.macro.mall.common.constant.AuthConstant;
import com.macro.mall.component.RestAuthenticationEntryPoint;
import com.macro.mall.component.RestfulAccessDeniedHandler;
import com.macro.mall.filter.IgnoreUrlsRemoveJwtFilter;
import lombok.AllArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.SecurityWebFiltersOrder;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
import org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtAuthenticationConverterAdapter;
import org.springframework.security.web.server.SecurityWebFilterChain;
import reactor.core.publisher.Mono;
/**
* 资源服务器配置
* Created by macro on 2020/6/19.
*/
@AllArgsConstructor
@Configuration
@EnableWebFluxSecurity
public class ResourceServerConfig {
private final AuthorizationManager authorizationManager;
private final IgnoreUrlsConfig ignoreUrlsConfig;
private final RestfulAccessDeniedHandler restfulAccessDeniedHandler;
private final RestAuthenticationEntryPoint restAuthenticationEntryPoint;
private final IgnoreUrlsRemoveJwtFilter ignoreUrlsRemoveJwtFilter;
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http.oauth2ResourceServer().jwt()
.jwtAuthenticationConverter(jwtAuthenticationConverter());
//自定义处理JWT请求头过期或签名错误的结果
http.oauth2ResourceServer().authenticationEntryPoint(restAuthenticationEntryPoint);
//对白名单路径,直接移除JWT请求头
http.addFilterBefore(ignoreUrlsRemoveJwtFilter,SecurityWebFiltersOrder.AUTHENTICATION);
http.authorizeExchange()
.pathMatchers(ArrayUtil.toArray(ignoreUrlsConfig.getUrls(),String.class)).permitAll()//白名单配置
.anyExchange().access(authorizationManager)//鉴权管理器配置
.and().exceptionHandling()
.accessDeniedHandler(restfulAccessDeniedHandler)//处理未授权
.authenticationEntryPoint(restAuthenticationEntryPoint)//处理未认证
.and().csrf().disable();
return http.build();
}
@Bean
public Converter<Jwt, ? extends Mono<? extends AbstractAuthenticationToken>> jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
jwtGrantedAuthoritiesConverter.setAuthorityPrefix(AuthConstant.AUTHORITY_PREFIX);
jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName(AuthConstant.AUTHORITY_CLAIM_NAME);
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);
return new ReactiveJwtAuthenticationConverterAdapter(jwtAuthenticationConverter);
}
}
Github:https://github.com/macrozheng/mall mall项目是一套电商系统,包括前台商城系统及后台管理系统,基于SpringBoot+MyBatis实现,采用Docker容器化部署。 前台商城系统包含首页门户、商品推荐、商品搜索、商品展示、购物车、订单流程、会员中心、客户服务、帮助中心等模块。 后台管理系统包含商品管理、订单管理、会员管理、促销管理、运营管理、内容管理、统计报表、财务管理、权限管理、设置等模块
Github:https://github.com/macrozheng/mall-admin-web mall-admin-web是一个电商后台管理系统的前端项目,基于Vue+Element实现。 主要包括商品管理、订单管理、会员管理、促销管理、运营管理、内容管理、统计报表、财务管理、权限管理、设置等功能
Github:https://github.com/macrozheng/mall-swarm mall-swarm是一套微服务商城系统,采用了 Spring Cloud Hoxton & Alibaba、Spring Boot 2.3、Oauth2、MyBatis、Docker、Elasticsearch等核心技术,同时提供了基于Vue的管理后台方便快速搭建系统。mall-swarm在电商业务的基础集成了注册中心、配置中心、监控中心、网关等系统功能。文档齐全,附带全套Spring Cloud教程
Github:https://github.com/macrozheng/springcloud-learning 一套涵盖大部分核心组件使用的Spring Cloud教程,包括Spring Cloud Alibaba及分布式事务Seata,基于Spring Cloud Greenwich及SpringBoot 2.1.7。21篇文章,篇篇精华,32个Demo,涵盖大部分应用场景
Github:https://github.com/macrozheng/mall-learning 项目中文文档:http://www.macrozheng.com/#/README